Defend Your Friend

Defend Your Friend is a puzzle game made in Unreal Engine 4, of which development started at the NHTV university in Breda (The Netherlands) in November 2015. After three semesters as a student project it transitioned into into a commercial project with seven of its original members left (including me); it is currently still in development and slated for release late 2017.

The game itself is built around casual puzzles heavily inspired by Portal 2 and the Trine series. There is no tutorial: Defend Your Friend is meant to be accessible to anyone and can act as a tool to introduce a non-experienced friend or family member to gaming.

Defend Your Friend in short:

  • Puzzle game made in Unreal Engine 4; two players with local and online co-op
  • My participation: November 2015 – present
  • Approved on Steam Greenlight
  • Developer team size varied from 7 to 20, currently consists of:
    • Three programmers (including me)
    • Two designers
    • Two artists

My main contributions

As a generalist programmer I was involved in most of the project’s code in some way. My main contributions however were related to the characters and gathering of gameplay statistics. Some networking tasks were the replication of character movement and aiming, as well as the interpolation of door movements.

  • Character Movement
  • Networking
  • Character Abilities
    • Smoothed aiming
    • Aim Assist
  • Gameplay statistics
    • Gameplay data (playtime per level, aim accuracy, etc) stored in online database
    • Heatmap of deaths (per play session, per level, per character)
    • Heatmap of performance (per sublevel, with average and lowest FPS)

Aim Smoothing

The orange line indicates the direction of the controller input

Aiming is a big part of the game and has therefore gone through the most iterations of any feature in Defend Your Friend. The current implementation dynamically increases the rotation speed depending on how big the angle is between the current and desired rotation.

Veterans and inexperienced players have expressed different wishes for responsiveness of their input during play testing: beginners mostly liked their mistakes or shaky fingers being masked by more indirect controls, while more experienced players generally appreciated their input being displayed immediately.

Therefore I implemented three presets: beginner, intermediate and expert. The main difference is in the rotation speed when close to the target. The ‘beginner’ preset has a lot of ‘drag’ at the end, which hides input inconsistencies from players who are inexperienced with controllers. Especially at long distances and when multiple mirrors are involved, the lower minimum rotation speed makes it easier to stay locked on a target.

Note that the character always rotates along the shortest path, so the angle can never be greater than 180 degrees. The formula for the dynamic rotation speed is as follows:

float ActualRotationSpeed = MinimumRotationSpeed + RotationSpeed * DotProduct(DesiredAimDirection, CurrentAimDirection)

By leveraging the cosine from the dot product, big rotations can still be done quite fast. Once the aim direction approaches the target, it gets slowed significantly for the lower sensitivities. This makes it easier for inexperienced players to hit their target, especially over long distances or when multiple mirrors are involved.

Automatic Aim

The only player input in this video is the movement

The automatic aiming is mainly meant to serve as subtle aim assist; the functionality shown in the video demonstrates how the system knows where the player’s aim should be assisted towards.

Solutions are found by using nested mirror projections, as shown in the concept image below. The system is based on a given target, and uses a set of relevant mirrors to find successful paths. First all possible unique combinations of mirrors are found and stored into an array, which is then sorted by length (so smaller combinations get queried first).

The amount of solutions is (2^N) - 1, where N is the amount of mirrors. Luckily all levels in the game have fewer than 5 relevant mirrors at a time, making the possible solutions less than 32. Even with 6 mirrors as shown in the video above, there is no noticeable performance impact. In cases where there would be a significantly larger amount of mirrors to consider, it can be beneficial to do the calculations asynchronously. That would actually be relatively easy to do with the current implementation.

Automatic Aim: Concept

[1] Project target through mirror 1
[2] Project projected target through mirror 2
[3] Let player fire at final projected target

Automatic Aim: Code Snippet

Before this code is run, a target is acquired and the relevant mirrors are stored. Then for each unique combination of mirrors (starting with smaller combinations) the code below is run. WillTargetBeHit() traces the path to see if the target is actually reachable; after all, the calculations themselves do not account for obstacles and assume the mirrors are infinitely long.

bool ADyfCharacterBase::FindLineToActor(TArray<ADyfMirrorBase*> aMirrorsToConsider, AActor* aTarget, FVector& aDirection, bool aDebug)
{
	FVector tProjectedTarget = aTarget->GetActorLocation();
	
	//The order of the reflection calculation is irrelevant
	for (auto tMirror : aMirrorsToConsider)
	{
		//Plane can be represented as vector since the game is 2D, and the plane is always perpendicular to the screen
		FVector tMirrorPlane = tMirror->GetActorRightVector();
		FVector tMirrorPosition = tMirror->GetActorLocation();
		FVector tToProjectedTarget = tProjectedTarget - tMirrorPosition;

		/*Find reflection point in mirror, so we know where to aim to hit the target with a reflection:
		1. Find angle from mirror to target (dot product on normalized vectors == cos(angle) between them)
		2. Given that angle and the distance to the target, calculate the distance along the mirror plane by multiplying it with cos(angle)
		3. Add that distance vector to the mirror position to get the projected point on the mirror plane
		4. Project the target into the mirror using that perpendicular point -> Angle(i) == Angle(r)*/

		float tCosAngle_TargetToMirror = FVector::DotProduct(tToProjectedTarget.GetSafeNormal(), tMirrorPlane);
		FVector tPointOnMirrorPlane = tMirrorPosition + tMirrorPlane * tCosAngle_TargetToMirror * tToProjectedTarget.Size();
		FVector tTargetToMirror = tPointOnMirrorPlane - tProjectedTarget;

		//Position the target 'inside' the mirror
		tProjectedTarget += 2 * tTargetToMirror;

		if (aDebug) {
			//Perpendicular line from crystal onto mirror
			DrawDebugLine(GetWorld(), aTarget->GetActorLocation(), tPointOnMirrorPlane, FColor::Red, false, 0.05f, 10, 10);
			//Player position 'in' the mirror
			DrawDebugSphere(GetWorld(), tProjectedTarget, 64.0f, 8, FColor::Red, false, 0.05f, 10, 10.0f);
		}
	}

	FVector tCharacterLocation = this->GetActorLocation();
	aDirection = (tProjectedTarget - tCharacterLocation).GetSafeNormal();
	
	//Check if target will be hit with the current aim direction
	return  WillTargetBeHit(tCharacterLocation, aTarget, aDirection);
}

Gameplay Statistics

The following gameplay data is gathered per level section:

  • Time for completion (general, and for both players individually)
  • Amount of times abilities have been activated (per player)
  • Total time abilities were active (per player)
  • Total time spent hitting a crystal and door (per player; can calculate accuracy with above values))
  • Amount of times each player was hit by a laser
  • Amount of times each player was hit by an enemy
  • Amount and location of deaths (per player)
  • Average FPS, lowest FPS and highest frame time (both the value and the location in the level)

Database:

In order to make sure the levels play out the way we intended them to, several data gathering systems are integrated into the game to keep track of how the players and hardware is doing. All the data is stored online into a database, which is accessed by Unreal through HTTP posts. This way users do not access the database directly from their system.

On BeginPlay, the user requests a session ID from the server. This session number and his username then get linked to data from each sublevel: there are triggers in the levels which send JSON blobs of data to the PHP webserver, which then inserts it into the database. Deaths are also logged per session, meaning that we can inspect every player’s experience individually.

A live version of the database front-end can be viewed here.

Approximate data usage, with 100 users completing 13 level sections in an average of 2 sessions: 188,1 KB

  • 100 users: 100 * 49 bytes = 4.90 KB
  • 200 sessions: 200 * 30 bytes = 6.00 KB
  • data logs: 13 * 100 * 136 bytes = 176.8 KB

The relational database ERD

Heatmaps: Deaths

Red dots are places where the player with the laser died, the blue dots for the shield player. They are logged per level and linked to a player’s session, so we can see where the players died in each specific play-through.

A good distribution of deaths:

An abnormally poor distribution of deaths at the bottom, caused by unclear level design:

Heatmaps: FPS

The yellow dots are the locations of the camera where the FPS was the lowest; the red dots are where the highest frame time occurred. When they are evenly spread across the level, the experience should be good for the player. When the circles bunch up in certain areas, the team knows exactly where the optimizations need to go.

A level with equally distributed performance:

The right side of this level has localized performance problems: