Recording KOPI

We’re working on KOPI trailer, which needs a lot of in-game vide shots (yes, much more than we thought, there’s a pattern emerging!). The initial naive thoughts of just capturing game play of the two of us playing very quickly turned out to be completely insufficient: even at 4k, the gameplay capture isn’t as crisp as we wanted, players as amazing as us also make mistakes that really stand out in the capture, and the biggest problem is that the captures were non-repeatable, non-tweakable. It was time for something else.

Alternative use for the test code

As part of our test code, we had the gameplay repeating controller: we record all important gameplay events and save them in a log that can later be re-played. We thought that we could directly use this recorder to record well-rehearsed gameplay and then “simply” set up the shot camera and record through the Level Sequencer. This sort sort of worked, but the problem was that looking through a different camera often necessitated slightly different positioning, making the recording unusable.

Even though the map is the same, the camera setup in the trailer shot makes it really hard to record a smooth natural gameplay.

The alternative–that is recording using the final camera angle is a complete no-go: the control axes are all rotated, but if even awesome gamers like us could overcome that, there is no way to pick up the items that need to go on the grill. They’re outside of the camera view.

Cinematic controller

In the end, we added another Unreal module to our game: KopiCinematics; we set the module type to be editor and set the dependency on the KopiGame module. From the top-level, our KOPI is made up of these four modules.

This way, we can make a shipping build of the game that definitely does not include any test code, because the KopiTests module is not included, and the test code is wrapped in #if WITH_TESTS ... #endif blocks–we don’t want nasty surprises where the shipping build includes code that was never intended to be there: think various debug messages or worse.

The KopiCinematics module actually only contains two important classes: AKopiCinematicsDriver and UKopiCinematicsPlayerController. The driver contains Blueprint exposed instructions that the player controller will perform.

Looking at the expanded sections of this “script”, we can see that the driver will drive the barista as follows:

  • Give the barista a container with raw bacon
  • Walk the barista to the grill, have the barista interact with the grill

In code, the structure looks like this:

UCLASS(Abstract)
class KOPICINEMATICS_API UKopiCinematicsInstruction : public UObject {
	GENERATED_BODY()
protected:
	UPROPERTY(EditAnywhere)
	float Time = 0.f;

public:
	virtual void Execute(AKopiBarista* Barista, 
	                     UKopiCinematicsPlayerController* Controller);
};

UENUM(Blueprintable)
enum class EKopiCinematicsInstructionActorAction : uint8 {
	WalkTo,
	Interact
};

UCLASS(EditInlineNew, DefaultToInstanced, CollapseCategories)
class KOPICINEMATICS_API UKopiCinematicsInstructionActor : public UKopiCinematicsInstruction {
	GENERATED_BODY()
protected:
	UPROPERTY(EditAnywhere)
	AActor* Actor;

	UPROPERTY(EditAnywhere)
	EKopiCinematicsInstructionActorAction Action = 
	  EKopiCinematicsInstructionActorAction::WalkTo;

public:
	virtual void Execute(AKopiBarista* Barista, 
	                     UKopiCinematicsPlayerController* Controller) override;
};

UCLASS(EditInlineNew, DefaultToInstanced, CollapseCategories)
class KOPICINEMATICS_API UKopiCinematicsInstructionLastContainer : public UKopiCinematicsInstruction {
	GENERATED_BODY()
protected:
	UPROPERTY(EditAnywhere)
	EKopiCinematicsInstructionActorAction Action = EKopiCinematicsInstructionActorAction::WalkTo;

	UPROPERTY(EditAnywhere)
	float YawDelta;

	UPROPERTY(EditAnywhere)
	FVector Offset;
public:
	static AActor* LastContainer;

	virtual void Execute(AKopiBarista* Barista, 
	                     UKopiCinematicsPlayerController* Controller) override;
};

UCLASS(EditInlineNew, DefaultToInstanced, CollapseCategories)
class KOPICINEMATICS_API UKopiCinematicsInstructionGiveContainer : public UKopiCinematicsInstruction {
	GENERATED_BODY()
protected:
	UPROPERTY(EditAnywhere)
	TSubclassOf<AKopiContainer> ContainerClass;
	UPROPERTY(EditAnywhere)
	TArray<UKopiIngredient*> Ingredients;
	UPROPERTY(EditAnywhere)
	bool bSetRandomData = true;

public:
	virtual void Execute(AKopiBarista* Barista, 
	                     UKopiCinematicsPlayerController* Controller) override;
};

UCLASS(EditInlineNew, DefaultToInstanced, CollapseCategories)
class KOPICINEMATICS_API UKopiCinematicsInstructionRubbish : public UKopiCinematicsInstruction {
	GENERATED_BODY()
protected:
	UPROPERTY(EditAnywhere)
	float YawDelta = {};

	UPROPERTY(EditAnywhere)
	EKopiCinematicsInstructionActorAction Action = 
	  EKopiCinematicsInstructionActorAction::WalkTo;

public:
	virtual void Execute(AKopiBarista* Barista, 
	                     UKopiCinematicsPlayerController* Controller) override;
};

UCLASS()
class KOPICINEMATICS_API AKopiCinematicsDriver : public AActor {
	GENERATED_BODY()

protected:
	UPROPERTY(EditAnywhere, Category="Kopi|Cinematics")
	int PlayerId = 0;

	UPROPERTY(EditAnywhere, Category="Kopi|Cinematics")
	USkeletalMesh* OverrideSkeletalMesh = {};

	UPROPERTY(EditAnywhere, Category="Kopi|Cinematics")
	float InitialDelay = 1.0f;

	UPROPERTY(EditAnywhere, Instanced, Category="Kopi|Cinematics")
	TArray<UKopiCinematicsInstruction*> Instructions;

	UPROPERTY(EditAnywhere, Category="Kopi|Cinematics")
	TArray<UKopiRecipe*> Recipes;

	UPROPERTY(EditAnywhere, Category="Kopi|Cinematics")
	TArray<FVector> RubbishLocations;

	virtual void BeginPlay() override;

public:
	AKopiCinematicsDriver();
	virtual void Tick(float DeltaSeconds) override;
};

The specific UCLASS modifiers on the UKopiCinematicsInstruction and UPROPERTY modifiers on AKopiCinematicsDriver::Instructions make it possible to conveniently edit the values in the level editor. The implementations are all fairly straight-forward; for example, the UKopiCinematicsInstructionGiveContainer spawns container with the requested ingredients and “gives” it to the barista.

void UKopiCinematicsInstructionGiveContainer::Execute(
  AKopiBarista* Barista, 
  UKopiCinematicsPlayerController* Controller) {
  
	const FTransform Transform = Barista->GetActorTransform();
	AKopiContainer* Container = Cast<AKopiContainer>(
	  GetWorld()->SpawnActor(ContainerClass, &Transform));
	UKopiCinematicsInstructionLastContainer::LastContainer = Container;
	for (const UKopiIngredient* Ingredient : Ingredients) {
		FVector Data = FVector::OneVector;
		Container->AddIngredient(FKopiIngredientInstance(Ingredient, Data));
	}
	Container->AttachToActor(Barista, 
	  FAttachmentTransformRules::SnapToTargetIncludingScale);
}

There is a little bit more complexity in the UKopiCinematicsPlayerController, not just because we want the player to move along as if controlled by human player controller, but because we want to trigger the interactions “directly”, not through an enhanced action input. Luckily, when we build for the Development targets, we #define WITH_TESTS, and so we can use the test code.

void UKopiCinematicsPlayerController::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) {
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	AKopiBarista* Barista = Cast<AKopiBarista>(GetOwner());
	
	...

	if (bInteract) {
		Barista->Test_SetInteractionTarget(Target);
		const FKopiExecuteInteractionResult& Result = 
		  Barista->Test_ExecuteInteraction(ETriggerEvent::Completed);
		bIsCompleted                                = Result.bCompleted;
		Target                                      = nullptr;
	} else {
		bIsCompleted = true;
		Target       = nullptr;
	}
}

Where the Test_SetInteractionTarget and Test_ExecuteInteraction methods are wrapped in the #if WITH_TESTS ... #endif bock.

Recording

The final piece of the puzzle is to actually record–do not forget that if you want to include the character in your level sequencer, you must remember to enable the relevant options.

The result

Without any further commentary, here’s the result. Oh, did I mention that we were nominated for the Best Small Studio award? No? Well, we were 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *