Blast Till Last

Game Project

Download

Description

A couch co-op game in the spirit of the brawler genre, which can be enjoyed by 2 to 4 players. This was an amazingly fun project to work on, and though our team faced different hurdles, we managed to make something remarkably entertaining out of it.

In this round-based game, each player controls a character with the ability to create shockwaves and blast each other off the map. To win a round one must be the last one standing, and to win the game one must win a number of rounds in total. To support the main goal of bombarding your opponents, random pick-ups spawn all around the map which varies from shockwave boosts to grenade launchers. Be cautious though, since your actions have an impact on a destructible environment responsible for more chaos and strategy which fuels the soul of this game.

Our game was also chosen to participate in a contest at The Rookies, which can be viewed here.

My Contributions

The Team

Artist – Bert Vanhengel
Artist – Alexander Windels
Programmer – Jarne Peire
Programmer – Maxim Dudich

Code Snippets

Camera: Clamping Springarm
void AC_A_SharedCamera::UpdateClamp()
{
	//Vector to store distances between players
	std::vector<float> distances;

	//Push in the m_MinClamp as distance incase there's 1 player left (there wont be two players to calculate the distance from)
	distances.push_back(m_MinClamp);

	//Gets the distance between all player possibilities in a vector
	for (int i = 0; i < m_Players.Num() - 1; i++)
	{
		if (m_Players[i].IsAlive)
		{
			for (int j = 0; j < m_Players.Num() - i - 1; j++)
			{
				if (m_Players[i + j + 1].IsAlive)
				{
					distances.push_back(FVector::Distance(m_Players[i].Location, m_Players[i + j + 1].Location));
				}
			}
		}
	}

	//Get the maximum distance between two players
	m_MaximumDistance = *std::max_element(distances.begin(), distances.end());

	//Adjust the camera spring arm according to that maximum distance
	AdjustCameraSpringArm(m_MaximumDistance);
}

void AC_A_SharedCamera::AdjustCameraSpringArm(float distance)
{
	float clampedDistance = FMath::Clamp(distance, m_MinClamp, m_MaxClamp);
	m_pCameraSpringArm->TargetArmLength = clampedDistance;
}
Camera: Translating To Players
void AC_A_SharedCamera::UpdateCameraLocation(float deltaTime)
{
	//Loop over all active players
	FVector targetLocation{ 0,0,0 };
	float amountOfAdds{ 0 };
	for (int i = 0; i < m_Players.Num(); i++)
	{
		//Add their locations to form a total vector
		if (m_Players[i].IsAlive)
		{
			targetLocation += m_Players[i].Location;
			amountOfAdds++;
		}
	}

	//Divide by the number of alive players to get an average location
	//Here is where the camera is centered to get an optimal view of everyone
	const FVector avgLocation = FVector(targetLocation.X / amountOfAdds, targetLocation.Y / amountOfAdds, targetLocation.Z / amountOfAdds);

	//Ease camera to that new location
	const FVector newCameraLocation = FMath::VInterpTo(m_CameraPreviousLocation, avgLocation, deltaTime, m_InterpSpeed);
	SetActorLocation(newCameraLocation);

	//Set previous location to the current one for next time
	m_CameraPreviousLocation = GetActorLocation();

	//Additional check for Z-axis offset
	if (m_UseZOffset)
	{
		float armLength{};
		for (int i = 0; i < m_Players.Num(); i++)
		{
			if (m_pCameraSpringArm->TargetArmLength < m_ZValueToStopOffset && m_Players[i].Location.Z > m_JumpHeightToStartZOffset)
			{
				//Clamping the arm length in case the distance from player to camera is smaller than the min. allowed value
				armLength = (m_pCameraSpringArm->TargetArmLength < m_MinClamp) ? m_MinClamp : m_pCameraSpringArm->TargetArmLength;

				//We need a percentage of the additional offset to add depending on the max. value to stop ofsetting
				const float ratio = armLength / m_ZValueToStopOffset;
				const float zOffset = m_ExtraZOffset - (ratio * m_ExtraZOffset);

				//Set the correct offset vector
				FVector cameraLocation = GetActorLocation();
				cameraLocation.Z += zOffset;

				//Update camera position
				const FVector offsettedCameraLocation = FMath::VInterpTo(m_CameraPreviousLocation, cameraLocation, deltaTime, m_InterpSpeed);
				SetActorLocation(offsettedCameraLocation);

				//Set previous location to the current one for next time
				m_CameraPreviousLocation = GetActorLocation();
			}
		}
	}

	//Additional check for Y-axis offset
	if (m_UseYOffset)
	{
		float previousDistancePlayerToCamera{};
		for (int i = 0; i < m_Players.Num(); i++)
		{
			const float distancePlayerToCamera = FVector::Distance(m_Players[i].Location, GetActorLocation());
			if (m_MaximumDistance > distancePlayerToCamera)
			{
				if (previousDistancePlayerToCamera < distancePlayerToCamera)
				{
					//Give extra offset on the Y-axis
					previousDistancePlayerToCamera = distancePlayerToCamera;

					//Determine the offset to add to the current camera location
					FVector cameraLocation = GetActorLocation();
					float yOffset = (m_MaximumDistance - distancePlayerToCamera) / 4.f;

					//Clamp it to a maximum value
					if (yOffset > m_MaximumYOffset)
					{
						yOffset = m_MaximumYOffset;
					}
					cameraLocation.Y += yOffset;

					//Set new camera location
					const FVector offsettedCameraLocation = FMath::VInterpTo(m_CameraPreviousLocation, cameraLocation, deltaTime, m_InterpSpeed);
					SetActorLocation(offsettedCameraLocation);

					//Set previous location to the current one for next time
					m_CameraPreviousLocation = GetActorLocation();
				}
			}
		}
	}
}
Hovering Pickups
// Called every frame
void UC_SC_HoverComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	const FVector currentLocation = GetComponentLocation();
	const FVector endLocation = FVector(currentLocation.X, currentLocation.Y, currentLocation.Z - m_TraceLength);

	FHitResult hit;
	if (GetWorld()->LineTraceSingleByChannel(hit, currentLocation, endLocation, ECollisionChannel::ECC_WorldStatic))
	{
		if (!hit.Actor->ActorHasTag("Player"))
		{
			//Get length from the vector starting at the component to where it hit something (the ground)
			float lineVectorLength = (hit.Location - currentLocation).Size();

			//Need to turn this value into percentage (0-1) of how much it covers our trace length
			//Since it needs to be used as an alpha for the lerp
			lineVectorLength /= m_TraceLength;

			//The closer to the ground, the more force it will take to apply to our object
			//If value surpasses our trace length, then the force will be 0 and the object will fall
			const float forceAmount = UKismetMathLibrary::Lerp(m_MaxForce, 0, lineVectorLength);
			
			//We only want our force to go up (Z-axis)
			FVector force = hit.ImpactNormal * forceAmount;
			force.X = 0;
			force.Y = 0;
			m_PreviousForce = force; //Needed in case we hit the player

			//Add force and damping
			m_PrimitiveComponent->AddForce(force);
			m_PrimitiveComponent->SetLinearDamping(m_LinearDamp);
			m_PrimitiveComponent->SetAngularDamping(m_AngularDamp);
		}
		else
		{
			//Possible to hit the character right when it walks over the pickup
			//Continue with the previous force and damping
			m_PrimitiveComponent->AddForce(m_PreviousForce);
			m_PrimitiveComponent->SetLinearDamping(m_LinearDamp);
			m_PrimitiveComponent->SetAngularDamping(m_AngularDamp);
		}
	}
}
Color Cycle (Customizing Character Colors)
void AC_A_ColorCycle::UpdateMaterialColor(float DeltaTime)
{
	//Increase the blend alpha between colors
	m_Blend += m_Sign * (DeltaTime / m_SlowDownRate);

	//Each cycle represents going from one color channel to the other
	//E.g. Cycle 0 (red) -> Cycle 1 (green)
	//E.g. Cycle 1 (green) -> Cycle 2 (blue)

	if (m_Blend >= 1.f && m_Cycle == 0)
	{
		m_Sign = -1;
		m_Cycle = 1;
		m_DynamicMaterial->SetVectorParameterValue(TEXT("Color1"), FLinearColor::Blue);
	}
	if (m_Blend <= 0.f && m_Cycle == 1)
	{
		m_Sign = 1;
		m_Cycle = 2;
		m_DynamicMaterial->SetVectorParameterValue(TEXT("Color2"), FLinearColor::Red);
	}
	if (m_Blend >= 1.f && m_Cycle == 2)
	{
		m_DynamicMaterial->SetVectorParameterValue(TEXT("Color1"), FLinearColor::Red);
		m_DynamicMaterial->SetVectorParameterValue(TEXT("Color2"), FLinearColor::Green);

		m_Blend = 0.f;
		m_Sign = 1;
		m_Cycle = 0;
	}

	//Return the current colors stored in the material that are being blended
	FLinearColor materialColor1;
	m_DynamicMaterial->GetVectorParameterValue(TEXT("Color1"), materialColor1);
	const FVector color1 = FVector(materialColor1.R, materialColor1.G, materialColor1.B);

	FLinearColor materialColor2;
	m_DynamicMaterial->GetVectorParameterValue(TEXT("Color2"), materialColor2);
	const FVector color2 = FVector(materialColor2.R, materialColor2.G, materialColor2.B);

	//Interpolate between colors
	m_LerpedColor = FMath::Lerp<FVector, float>(color1, color2, m_Blend);

	//Update material variables back to it updates the mesh
	m_DynamicMaterial->SetVectorParameterValue(TEXT("MainColor"), FLinearColor(m_LerpedColor));
	m_DynamicMaterial->SetScalarParameterValue(TEXT("Opacity"), m_Opacity);
	m_DynamicMaterial->SetScalarParameterValue(TEXT("Blend"), m_Blend);
}

void AC_A_ColorCycle::UpdateMeshCollision()
{
	TArray<AActor*> pActors;
	m_Mesh->GetOverlappingActors(pActors);

	for (int i = 0; i < pActors.Num(); i++)
	{
		//Return the player character currently in the color cycle mesh
		AC_CH_Player* pPlayerActor = Cast<AC_CH_Player>(pActors[i]);
		if (pPlayerActor)
		{
			//Update previously calculated color
			pPlayerActor->SetNewCharacterColor(m_LerpedColor);

			//Saves data in a persistant manner between the levels
			UC_GI_PersistentData* pPerData{ dynamic_cast<UC_GI_PersistentData*>(GetGameInstance()) };
			if (pPerData)
			{
				uint32 playerID{};
				APlayerController* pPlayerController = pPlayerActor->GetController<APlayerController>();
				if (pPlayerController)
				{
					playerID = UGameplayStatics::GetPlayerControllerID(pPlayerController);
				}

				pPerData->m_PlayersUsingCustomColor[playerID] = true;
				pPerData->m_PlayersCustomColor[playerID] = m_LerpedColor;
			}
		}
	}
}

Trailer

Gameshots

Interactive Main Menu
In-Game Shots
Artwork of characters and pickups in-game