Back
Rouse
Info

Role: Gameplay, Animation & UI Programmer

Genre: Third Person Puzzle Shooter

Engine: Unreal Engine

Team Size: 12

Platform: PC

Time: 7 weeks

About

Rouse is a 3rd-person physics-based puzzle platformer. You play as Rose who finds herself trapped inside the dream of her little brother. The game's main mechanic is a gun that the player can use to shoot and physically affect elements in the map (freezing, inverting gravity or levitating and sticking).

Highlights
  • Third person aiming and physics based shooting with projectile trajectory.
  • Object grabbing and dragging.
  • System for puzzles.
  • Procedural animations using IK.
Aiming and Shooting

Trajectory

The projectile will always land where the player is aiming. The max height a projectile will reach (H) is the only variable that can be tweaked in this system where R is the distance to the point that's being aimed at. If we solve the following system of equations for Initial Velocity (v0) and Initial Angle (ϴ), we'll know the necessary impulse and direction to exert on the projectile to reach the point that is being aimed. I made this solver into a blueprint function library so that we could easily prototype it in BP aswell.

Property: Function:
Height (H) Projectile's max. height
Distance (R) Distance from projectile origin (gun) to aimed point
Initial Angle (ϴ) Necessary initial angle to reach aimed point
Initial Velocity (v0) Necessary impulse to reach aimed point
Code - Trajectory Solver
UCLASS()
    class GP3_API UT7PhysicsBlueprintLibrary : public UBlueprintFunctionLibrary
    {
        GENERATED_BODY()
    
    public:
        UFUNCTION(BlueprintCallable, Category="Projectiles")
        static float GetAngleRadians(float Range, float MaxHeight, float Gravity = 980.f)
        {
            return atan((4*MaxHeight)/Range);
        }
    
        UFUNCTION(BlueprintCallable, BlueprintPure, Category="Projectiles")
        static float GetAngleDegrees(float Range, float MaxHeight, float Gravity = 980.f)
        {
            return atan((4*MaxHeight)/Range) * 180/PI;
        }
    
        UFUNCTION(BlueprintCallable, BlueprintPure, Category="Projectiles")
        static float GetImpulseFromRadians(const float MaxHeight, const float AngleRadians, const float Gravity = 980.f)
        {
            return sqrtf((2*Gravity*MaxHeight)/sqr(sinf(AngleRadians)));
        }
    
        UFUNCTION(BlueprintCallable, BlueprintPure, Category="Projectiles")
        static float GetImpulseFromDegrees(const float MaxHeight, const float AngleDegrees, const float Gravity = 980.f)
        {
            return sqrtf((2*Gravity*MaxHeight)/sqr(sinf(AngleDegrees*(PI/180.f))));
        }
    
        UFUNCTION(BlueprintCallable, BlueprintPure, Category="Projectiles")
        static FVector GetAdjustedGravity(const FVector AimUpVector, const float Gravity = 980.f)
        {
            return -AimUpVector * Gravity;
        }
    };
    
Animation

Since we didn't have an animator on the team, we adapted by recurring to mostly procedural animation and using IK.

  • The character's upper body will yaw to face the aimed point. It will also pitch to match the initial angle that the projectile is being shot at.
  • The character's bones are individually translated and rotated to simulate recoil.
  • The character will also move its body (both lower and upper) depending on the speed that camera is being turned.

UI

  • Dynamic crosshair that will sync to the gun's firerate. Crosshair will play a rotation animation when aim target is in range but will be static and lower its opacity if target is not in range.
  • When pointing at an interactable that is affected by a power, a circle will show that will be filled on player retract.
  • Gameboy shows color of selected power and its corresponding ammo amount in world space.
  • Some more animations and visal cues are used to communicate when a retract is done or when shooting with no ammo.

Grabbing & Dragging

The behaviour for this mechanic is mostly inspired by Little Nightmares' dragging system where the player needs to interact with an object to grab it and only then can drag it around. The object will be dropped if that same button is pressed or if it goes out of range.

Two actor components are used on this system. One on the object that can be pushed and another one on the entity that can push objects. This pattern allows for a modular system where new objects could be very easily added and be pushed around by the existing or new characters.

Little Nightmares' Drag System
Code - Push Component
void UT7PushComponent::TryPush(AActor* ActorToPush)
{
	// Not interacting with valid actor
	if (ActorToPush == nullptr) return;
	UT7PushableComponent* PushableComp = ActorToPush->FindComponentByClass<UT7PushableComponent>();
	// Actor isn't pushable
	if (PushableComp == nullptr) return;
	
	Pushable = PushableComp;
	// Check if pushable is grounded
	if (!Pushable->bIsGrounded)
	{
		LOG("Can't push, object isn't grounded...");
		return;
	}
	// Evaluate which side is being pushed
	bIsOverlapping = Pushable->EvaluateOverlap(GetOwner());
	if (!bIsOverlapping)
	{
		LOG("Can't push, not overlapping pushable...")
		return;
	}
	if (!Pushable->bCanPush)
	{
		LOG("Can't push, object isn't pushable...")
		return;
	}
	
	// Can push...
	bIsPushing = true;

	LOG("Start push!");
	// Move pushable away from actor
	bLerpToLocation = true;
	Cast<AT7Character>(GetOwner())->bLockMovement = true;

	// Target location and rotation
	TargetLocation = GetOwner()->GetActorLocation() + Pushable->BoxSide * PushOffset;
	TargetRotation = UKismetMathLibrary::MakeRotFromX(-Pushable->BoxSide);
	
	// Calculate direction to pushable
	PushableLocationOnStart = Pushable->GetOwner()->GetActorLocation();
	
	// Disable physics
	Pushable->PrimitiveComponent->SetSimulatePhysics(false);

	if (Pushable->DragMultiplier != 0.f)
	{
		// Raise from ground
		Pushable->GetOwner()->AddActorWorldOffset(FVector::UpVector * LiftDistance);
	}
	
	OnStartPush.Broadcast(Pushable);
}
Animation

Puzzle Interactions

The behaviour is inspired by an AND processor where all the output responses will get triggered if all buttons are pressed. The processor’s behaviour is easily customizable to work like a NAND, OR, XOR, etc.

Each button’s behaviour is very open to customization. Levers and sticky timed buttons were prototyped but didn’t make it to any puzzle in the final game.

An event is called on each response component being triggered which allows for a modular system. Multiple and diverse events can be called: Opening doors, UI prompts, particle effects, etc. These were exposed to BP and mostly worked on by designers.

Code - Interactable Processor
void UT7InteractableProcessor::TriggerActivate()
{
	if (OutputResponses.Num() == 0) return;
	
	for (const auto Output : OutputResponses)
	{
		if (Output == nullptr) continue;
		
		const UT7InteractableResponse* InteractableResp = Output->FindComponentByClass<UT7InteractableResponse>();
		if (InteractableResp == nullptr) continue;

		InteractableResp->OnActivate.Broadcast();
	}
}

void UT7InteractableProcessor::TriggerDeactivate()
{
	if (OutputResponses.Num() == 0) return;
	
	for (const auto Output : OutputResponses)
	{
		if (Output == nullptr) continue;
		
		const UT7InteractableResponse* InteractableResp = Output->FindComponentByClass<UT7InteractableResponse>();
		if (InteractableResp == nullptr) continue;

		InteractableResp->OnDeactivate.Broadcast();
	}
}
Main Takeaways
© 2022 João Freire. All rights reserved.