diff --git a/Content/M_PointOfInterest.uasset b/Content/M_PointOfInterest.uasset
new file mode 100644
index 0000000000000000000000000000000000000000..a8139e4ff912b49947eea83e25b273d05b8715fc
Binary files /dev/null and b/Content/M_PointOfInterest.uasset differ
diff --git a/Content/location_marker.uasset b/Content/location_marker.uasset
new file mode 100644
index 0000000000000000000000000000000000000000..9596735fa4f3699f4a0effa1340d9ad94508e6a9
Binary files /dev/null and b/Content/location_marker.uasset differ
diff --git a/Source/RWTHVRQuickStart/Private/PointOfInterest.cpp b/Source/RWTHVRQuickStart/Private/PointOfInterest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..30b7635e76c6d4e982f747e5f1e3b8ec7922d690
--- /dev/null
+++ b/Source/RWTHVRQuickStart/Private/PointOfInterest.cpp
@@ -0,0 +1,29 @@
+#include "PointOfInterest.h"
+
+#include "Logging/StructuredLog.h"
+#include "Components/BillboardComponent.h"
+
+DEFINE_LOG_CATEGORY(POILog)
+
+APointOfInterest::APointOfInterest()
+{
+	UE_LOGFMT(POILog, Verbose, "POI instantiated");
+
+	SetRootComponent(CreateDefaultSubobject<USceneComponent>("SceneComponent"));
+
+	auto Billboard = CreateDefaultSubobject<UBillboardComponent>("BillboardComponent");
+
+	static ConstructorHelpers::FObjectFinder<UTexture2D> LocationMarker(TEXT("/Script/Engine.Texture2D'/RWTHVRQuickStart/location_marker.location_marker'"));
+	if(LocationMarker.Succeeded())
+	{
+		Billboard->SetSprite(LocationMarker.Object);
+	}
+	else
+	{
+		UE_LOGFMT(POILog, Verbose, "POILog: Could not find location marker Texture. The Asset might have moved.");
+	}
+	Billboard->AttachToComponent(RootComponent, FAttachmentTransformRules::SnapToTargetIncludingScale);
+	Billboard->SetHiddenInGame(true);
+	Billboard->SetWorldScale3D(FVector(0.1f, 0.1f, 0.1f));
+}
+
diff --git a/Source/RWTHVRQuickStart/Private/PointOfInterestManager.cpp b/Source/RWTHVRQuickStart/Private/PointOfInterestManager.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e6a2ab18c2787aeb964647e75d406849d3533286
--- /dev/null
+++ b/Source/RWTHVRQuickStart/Private/PointOfInterestManager.cpp
@@ -0,0 +1,194 @@
+#include "PointOfInterestManager.h"
+
+#include "PointOfInterest.h"
+#include "Kismet/GameplayStatics.h"
+#include "Logging/StructuredLog.h"
+
+DEFINE_LOG_CATEGORY(POIManagerLog);
+
+APointOfInterestManager::APointOfInterestManager()
+{
+	PrimaryActorTick.bCanEverTick = true;
+	PrimaryActorTick.bStartWithTickEnabled = true;
+	PrimaryActorTick.SetTickFunctionEnable(true);
+
+	POIs = TArray<APointOfInterest*>();
+
+	SplineComponent = CreateDefaultSubobject<USplineComponent>("SplineComponent");
+	SplineComponent->SetupAttachment(RootComponent.Get());
+}
+
+void APointOfInterestManager::TickActor(float DeltaTime, enum ELevelTick TickType, FActorTickFunction& ThisTickFunction)
+{
+	SplineComponent->Duration = SplineComponent->GetSplineLength() / CameraSpeed;
+	if (IsRidingCamera)
+	{
+		ProgressCameraRide();
+	}
+}
+
+void APointOfInterestManager::AddPointOfInterest()
+{
+	APointOfInterest* POI = static_cast<APointOfInterest*>(GetWorld()->SpawnActor(APointOfInterest::StaticClass()));
+
+	TScriptDelegate Del = TScriptDelegate();
+	Del.BindUFunction(this, "UpdateSpline");
+	POI->GetRootComponent()->TransformUpdated.AddLambda([this](USceneComponent*, EUpdateTransformFlags, ETeleportType)
+	{
+		UpdateSpline();
+	});
+
+	POIs.Add(POI);
+
+	UpdateSpline();
+}
+
+void APointOfInterestManager::VisitNextPointOfInterest()
+{
+	if (CurrentPOIIndex >= GetPointOfInterestCount() - 1)
+	{
+		CurrentPOIIndex = 0;
+	}
+	else
+	{
+		++CurrentPOIIndex;
+	}
+	VisitPointOfInterest(POIs[CurrentPOIIndex]);
+}
+
+void APointOfInterestManager::VisitPreviousPointOfInterest()
+{
+	if (CurrentPOIIndex <= 0)
+	{
+		CurrentPOIIndex = GetPointOfInterestCount() - 1;
+	}
+	else
+	{
+		--CurrentPOIIndex;
+	}
+	VisitPointOfInterest(POIs[CurrentPOIIndex]);
+}
+
+void APointOfInterestManager::VisitPointOfInterestByIndex(int index)
+{
+	VisitPointOfInterest(POIs[index]);
+}
+
+
+void APointOfInterestManager::VisitPointOfInterest(APointOfInterest* POI) const
+{
+	APawn* Pawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
+	if (!Pawn)
+	{
+		UE_LOGFMT(POIManagerLog, Warning, "Attempted to move player pawn but no pawn found to move.");
+		return;
+	}
+	FVector Location = POI->GetActorLocation();
+	FRotator Rotation = POI->GetActorRotation();
+	Rotation.Pitch = 0;
+	Rotation.Roll = 0;
+
+	Pawn->Controller->SetControlRotation(Rotation);
+	Pawn->SetActorLocationAndRotation(Location, Rotation);
+}
+
+int APointOfInterestManager::GetPointOfInterestCount()
+{
+	return POIs.Num();
+}
+
+void APointOfInterestManager::StartCameraRide()
+{
+	CameraRideStart = UGameplayStatics::GetTimeSeconds(this);
+	CameraRideEnd = CameraRideStart + SplineComponent->Duration;
+
+	IsRidingCamera = true;
+}
+
+void APointOfInterestManager::StopCameraRide()
+{
+	CameraRideEnded();
+}
+
+
+void APointOfInterestManager::UpdateSpline()
+{
+	for (int i = 0; i < GetPointOfInterestCount(); ++i)
+	{
+		if (POIs[i] == nullptr)
+		{
+			POIs.RemoveAt(i);
+			SplineComponent->RemoveSplinePoint(i);
+			--i;
+		}
+		else if (!POIs[i]->GetRootComponent()->TransformUpdated.IsBound())
+		{
+			POIs[i]->GetRootComponent()->TransformUpdated.AddLambda(
+				[this](USceneComponent*, EUpdateTransformFlags, ETeleportType)
+				{
+					UpdateSpline();
+				});
+		}
+	}
+
+	int POIsNum = GetPointOfInterestCount();
+	int SplinePointsNum = SplineComponent->GetNumberOfSplinePoints();
+
+	if (POIsNum > SplinePointsNum)
+	{
+		for (int i = SplinePointsNum; i < POIsNum; ++i)
+		{
+			SplineComponent->AddSplinePointAtIndex(FVector::Zero(), i, ESplineCoordinateSpace::World);
+		}
+	}
+	else if (POIsNum < SplinePointsNum)
+	{
+		for (int i = SplinePointsNum - 1; i >= POIsNum; --i)
+		{
+			SplineComponent->RemoveSplinePoint(i);
+		}
+	}
+
+	for (int i = 0; i < GetPointOfInterestCount(); ++i)
+	{
+		APointOfInterest* POI = POIs[i];
+		FVector POIPosition = POI->GetActorLocation();
+		FRotator POIRotation = POI->GetActorRotation();
+		SplineComponent->SetLocationAtSplinePoint(i, POIPosition, ESplineCoordinateSpace::World);
+		SplineComponent->SetRotationAtSplinePoint(i, POIRotation, ESplineCoordinateSpace::World);
+	}
+}
+
+void APointOfInterestManager::ProgressCameraRide()
+{
+	double Current = UGameplayStatics::GetTimeSeconds(this);
+	float SecondsSinceStart = Current - CameraRideStart;
+	UE_LOGFMT(POIManagerLog, VeryVerbose, "");
+
+	APawn* Pawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
+	if (!Pawn)
+	{
+		UE_LOGFMT(POIManagerLog, Warning, "Attempted to move player pawn but no pawn found to move.");
+		return;
+	}
+	if (Current < CameraRideEnd)
+	{
+		FVector Location = SplineComponent->GetLocationAtTime(SecondsSinceStart, ESplineCoordinateSpace::World);
+		FRotator Rotation = SplineComponent->GetRotationAtTime(SecondsSinceStart, ESplineCoordinateSpace::World);
+		Rotation.Pitch = 0;
+		Rotation.Roll = 0;
+
+		Pawn->Controller->SetControlRotation(Rotation);
+		Pawn->SetActorLocationAndRotation(Location, Rotation);
+	}
+	else
+	{
+		CameraRideEnded();
+	}
+}
+
+void APointOfInterestManager::CameraRideEnded()
+{
+	IsRidingCamera = false;
+	VisitPointOfInterestByIndex(GetPointOfInterestCount() - 1);
+}
diff --git a/Source/RWTHVRQuickStart/Public/PointOfInterest.h b/Source/RWTHVRQuickStart/Public/PointOfInterest.h
new file mode 100644
index 0000000000000000000000000000000000000000..71f2139f90008f671978a255d81bece406ad6513
--- /dev/null
+++ b/Source/RWTHVRQuickStart/Public/PointOfInterest.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "CoreMinimal.h"
+
+#include "PointOfInterest.generated.h"
+
+DECLARE_LOG_CATEGORY_EXTERN(POILog, All, All)
+
+UCLASS()
+class APointOfInterest : public AActor
+{
+	GENERATED_BODY()
+
+public:
+	APointOfInterest();
+};
diff --git a/Source/RWTHVRQuickStart/Public/PointOfInterestManager.h b/Source/RWTHVRQuickStart/Public/PointOfInterestManager.h
new file mode 100644
index 0000000000000000000000000000000000000000..982f2374b7455e2b2641b62cbd3ebbf3f9b73934
--- /dev/null
+++ b/Source/RWTHVRQuickStart/Public/PointOfInterestManager.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Components/SplineComponent.h"
+
+#include "PointOfInterestManager.generated.h"
+
+DECLARE_LOG_CATEGORY_EXTERN(POIManagerLog, All, All);
+
+class APointOfInterest;
+class FCameraRide;
+
+UCLASS(Blueprintable)
+class APointOfInterestManager : public AActor
+{
+	GENERATED_BODY()
+
+public:
+	APointOfInterestManager();
+
+	virtual void TickActor(float DeltaTime, enum ELevelTick TickType, FActorTickFunction& ThisTickFunction) override;
+
+	UFUNCTION(CallInEditor, Category="Point Of Interest Manager")
+	void AddPointOfInterest();
+
+	UFUNCTION(CallInEditor, Category="Point Of Interest Manager")
+	void VisitNextPointOfInterest();
+
+	UFUNCTION(CallInEditor, Category="Point Of Interest Manager")
+	void VisitPreviousPointOfInterest();
+
+	void VisitPointOfInterestByIndex(int index);
+	void VisitPointOfInterest(APointOfInterest* POI) const;
+
+	UFUNCTION(CallInEditor, Category="Point Of Interest Manager")
+	void StartCameraRide();
+	UFUNCTION(CallInEditor, Category="Point Of Interest Manager")
+	void StopCameraRide();
+
+	int GetPointOfInterestCount();
+
+protected:
+	UPROPERTY(EditAnywhere)
+	TArray<APointOfInterest*> POIs;
+
+	int CurrentPOIIndex = 0;
+
+private:
+	UFUNCTION(CallInEditor, Category="Point Of Interest Manager")
+	void UpdateSpline();
+
+	void ProgressCameraRide();
+	void CameraRideEnded();
+
+	USplineComponent* SplineComponent;
+
+	UPROPERTY(EditAnywhere,  meta=(UIMin = 0.0, UIMax = 500))
+	float CameraSpeed = 100;
+	bool IsRidingCamera;
+	float CameraRideStart, CameraRideEnd;
+};
diff --git a/Source/RWTHVRQuickStart/RWTHVRQuickStart.Build.cs b/Source/RWTHVRQuickStart/RWTHVRQuickStart.Build.cs
index 894b0c6ab811c64470fa1dad70e6fda8b7a0553f..21f957f00a2fc6d0887f4f42b50215e6cae15878 100644
--- a/Source/RWTHVRQuickStart/RWTHVRQuickStart.Build.cs
+++ b/Source/RWTHVRQuickStart/RWTHVRQuickStart.Build.cs
@@ -25,7 +25,7 @@ public class RWTHVRQuickStart : ModuleRules
 		PublicDependencyModuleNames.AddRange(
 			new string[]
 			{
-				"Core",
+				"Core", "Engine", "Engine",
 				// ... add other public dependencies that you statically link with here ...
 			}
 			);
@@ -37,7 +37,7 @@ public class RWTHVRQuickStart : ModuleRules
 				"CoreUObject",
 				"Engine",
 				"Slate",
-				"SlateCore",
+				"SlateCore", "RWTHVRToolkit", "RWTHVRToolkit",
 				// ... add private dependencies that you statically link with here ...	
 			}
 			);