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
- Game Mechanics (Round System, Player Info)
- Camera Functionality
- Character Customization
- Character Animation (Blendspaces, Blueprints, Sockets)
- Base Functionality Pickups
- Pickup Hovering
- UI (Player Arrow, Occlusion Masking, Pickup Indication)
- HUD/Menu's
- Audio/Sound (+ self produced battle track)
The Team
Artist – Bert Vanhengel
Artist – Alexander Windels
Programmer – Jarne Peire
Programmer – Maxim Dudich
Code Snippets
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;
}
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();
}
}
}
}
}
// 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);
}
}
}
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;
}
}
}
}