Work

Project Doll

Unreal Engine 5
C++
Windows
Keyboard/Mouse

It's a prototype 3D Game in first person view. Where you search about your sister who is lost in the "isla de las munecas" (Island of the Dead Dolls). The antagonist is a Doll. She can move when you don't watch it or when your eye blink. It's like "Statues" or "SCP containment breach". The main reason i have create this project is to confirm myself about my C++ Skills in Unreal Engine and learn about Timeline and AI in C++.

Iridescent ripples of a bright blue and pink liquid

What I have learned

  • Handling Timeline in C++
  • Create an AI in C++ that move when you don’t watch it
  • Discover and use the Enhanced input
  • Create a blink eyes system
  • Create an Echolocation system

Enhanced input

It’s a plugin that’s allow you to have more complex input. To enable it, you have to go in the plugin menu and search for “Enhanced Input”. I have enabled it for 2 reasons:

  • To test it and understand it how it works
  • To bind the echolocation system with blink system in one input

He works with an Input Mapping Context, he describes the rules for triggering the different Inputs and link with an Input Action. An Input Action is the communication between the code and the Input Mapping context. In my project have 5 Inputs Actions:

  • Input Action Blink ⇒ Action to trigger the blink action
  • Input Action Camera ⇒ Inputs to move the camera with the mouse
  • Input Action Movements ⇒ inputs to move the player
  • Input Action Close Eye ⇒ Action to trigger the eye closed and keep it
  • Input Action Open Eye ⇒ Action to trigger the eye open.

The first thing to do is add an input context to a player controller

  
void APlayerChararcter::BeginPlay()
{
	Super::BeginPlay();
	if (APlayerController* PlayerController = Cast(Controller))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<
		UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(MappingContext, 0);
		}
	}
}

void APlayerChararcter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	if (UEnhancedInputComponent* PlayerEnhancedInputComponent = Cast(PlayerInputComponent))
	{
		PlayerEnhancedInputComponent->BindAction(MovementAction, ETriggerEvent::Triggered, this,
		                                         &APlayerChararcter::MovePlayer);
		PlayerEnhancedInputComponent->BindAction(CameraAction, ETriggerEvent::Triggered, this,
		                                         &APlayerChararcter::MoveCamera);
		PlayerEnhancedInputComponent->BindAction(HoldEyeAction, ETriggerEvent::Triggered, this,
		                                         &APlayerChararcter::ClosedEyeAction);
		PlayerEnhancedInputComponent->BindAction(ReleaseEyeAction, ETriggerEvent::Triggered, this,
		                                         &APlayerChararcter::OpenEyeAction);
		PlayerEnhancedInputComponent->BindAction(BlinkAction, ETriggerEvent::Completed, this,
		                                         &APlayerChararcter::Blink);
	}
}

void APlayerChararcter::MovePlayer(const FInputActionValue& Instance)
{
	if (!bIsHold)
	{
		const FVector AxisValue = Instance.Get();
		AddMovementInput(GetActorForwardVector() * AxisValue.X);
		AddMovementInput(GetActorRightVector() * AxisValue.Y);
	}
}

void APlayerChararcter::MoveCamera(const FInputActionValue& Instance)
{
	const FVector2D AxisValue = Instance.Get();
	AddControllerPitchInput(AxisValue.Y * -1);
	AddControllerYawInput(AxisValue.X);
}

  

Eye blink and blood effect

It’s the main mechanic of the prototype, every X seconds, the character must blink, and the antagonist is going to move towards you. He works with 2 timelines who manage the visual effect and 1 timer to trigger every thing with a rate of 1 second. At the end of the timeline, “Blink”, an Event is executed to unpause the timer and restart the cycle. The blood eye effect is used to show how soon the character is going to blink.

	
void APlayerChararcter::Blink()
{
	if (!bIsSpaceBarPressed)
	{
		bIsSpaceBarPressed = true;
		TimelineBlink->PlayFromStart();
		TimelineBloodEye->Reverse();
		GetWorldTimerManager().PauseTimer(TimerHandleCheckBlink);
		HowManyTimeBeforeBlink = TimeBeforeBlinkMax;
	}
}
	

Echolocation

This mechanic is a way to help the player to find any important object who implemented the echolocation system. He holds the space bar button, and then the character focused to find where any important object are revealed. However, The antagonist will still be moving towards you. To find every actor who need the echolocation system , I have created an Interface who enable/disable the visibility of the effect.

	
void APlayerChararcter::ClosedEyeAction(const FInputActionValue& Instance)
{
	if (!TimelineBlink->IsPlaying())
	{
		GetWorldTimerManager().PauseTimer(TimerHandleCheckBlink);
		HowManyTimeBeforeBlink = TimeBeforeBlinkMax;
		bIsBlink = true;
		bIsHold = true;
		TimelineBlink->PlayFromStart();
		DynamicMaterialBloodEyeEffect->SetScalarParameterValue("Lerp", 0);
		TimelineBloodEye->Stop();
	}
}

void APlayerChararcter::OpenEyeAction(const FInputActionValue& Instance)
{
	if (!TimelineBlink->IsPlaying() && bIsHold)
	{
		GetWorldTimerManager().UnPauseTimer(TimerHandleCheckBlink);
		bIsBlink = false;
		bIsHold = false;
		TimelineBlink->PlayFromStart();
		TimelineBloodEye->PlayFromStart();
	}
}

UINTERFACE(MinimalAPI)
class UIEcholocation : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class DOLLS_API IIEcholocation
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	
	UFUNCTION(BlueprintCallable,BlueprintImplementableEvent)
	void EnableVisibility(bool bIsSet);
};
	

A.I Doll

It’s the antagonist and the only enemy. She will follow you and try to get closer to kill you. She is only moving if you don’t watch her. So if you blink, you close your eyes, or you don’t look at her, she is going to move. I have created a service that check if she is render and if it’s the case, I check if the character got eyes closed.

	
UServiceCheckIsRendering::UServiceCheckIsRendering()
{
	NodeName = TEXT("Is Actor rendering");
}

void UServiceCheckIsRendering::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
	OwnerComp.GetBlackboardComponent()->SetValueAsBool(GetSelectedBlackboardKey(), CheckIfIsRender(OwnerComp));
}

bool UServiceCheckIsRendering::CheckIfIsRender(UBehaviorTreeComponent& OwnerComp) const
{
	const AAIController* Controller = OwnerComp.GetAIOwner();
	const APlayerChararcter* Player = Cast(UGameplayStatics::GetPlayerPawn(GetWorld(),0));
	bool bIsRender = Controller->GetPawn()->WasRecentlyRendered(0.1f);
	if(bIsRender)
	{
		bIsRender = !Player->GetIsCloseEyes();
	}
	UE_LOG(LogTemp, Warning, TEXT("%d"),bIsRender);
	OwnerComp.GetBlackboardComponent()->SetValueAsBool(GetSelectedBlackboardKey(), bIsRender);
	return bIsRender;
}