Shred-Off (2024)
Unreal Engine 5 - 12 developers - 8 months development time - Gameplay Programmer - PC
Unreal Engine 5 - 12 developers - 8 months development time - Gameplay Programmer - PC
This game was project I worked on all throughout my 3rd year at Breda University of Applied Sciences.
I, along with 11 other teammates, were tasked with creating a shooter that involved vehicles in a snowy environment.
We spent 8 weeks on pre-production, 20 weeks on production, and 4 weeks on post-production
After a lot of hard work, we published the game on Steam in June of 2024.
My biggest contribution was making a Snowboard Movement Component in C++. Unreal Engine 5 does not have a simple way to do snowboard movement so after, after some prototyping, as a team, we decided that we should make a component that would emulate the movement of the snowboard in C++. I decided to take this task head-on as I saw an opportunity for a good learning experience.
This is the code for the Snowboard Movement function:
void USnowboardMovementComponent::SnowboardMovement(FVector WorldVector, float& Speed, UStaticMeshComponent* Snowboard)
{
if (!Snowboard)
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
GEngine->AddOnScreenDebugMessage(-1, 200.f, FColor::Red, "ERROR:MOVE IN DIRECTION FUNCTION DOES NOT HAVE ALL VALUES");
#endif
return;
}
Direction += WorldVector;
Direction.Normalize();
Grounded = false;
for (int i = 0; i < Hits.Num(); i++)
{
if (Hits[i].bBlockingHit)// if touching ground reset values
{
if (Speed < 0)
{
Speed = -Speed;
}
if (CanJump > 0)
{
CanJump--;
}
Grounded = true;
if (!PreviousFrameGrounded)
{
ExtraMovement = FVector(0,0,0);
Direction = WorldVector;
}
break;
}
}
//Line trace for height to the ground
FCollisionQueryParams CollisionParams;
/*const FName DistanceToGroundTag("MyTraceTag");
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
GetWorld()->DebugDrawTraceTag = DistanceToGroundTag;
#endif
CollisionParams.TraceTag = DistanceToGroundTag; *///Uncomment for debug lines
CollisionParams.AddIgnoredActor(PawnOwner);
TArray<AActor*> IgnoreActors;
TArray<AActor*> ActorsFound;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AProjectile::StaticClass(), IgnoreActors);
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABulletShell::StaticClass(), ActorsFound);
IgnoreActors.Append(ActorsFound);
CollisionParams.AddIgnoredActors(IgnoreActors);
float lineLength = 500;
FVector lineStart = PawnOwner->GetActorLocation() - FVector(0,0,1)*75;
FHitResult HeightCheck;
GetWorld()->LineTraceSingleByChannel
(HeightCheck, //result
lineStart, //start
lineStart - (CurrentPlaneNormal * lineLength), //end
ECC_WorldDynamic, //collision channel
CollisionParams);
float DistanceBetweenGroundAndPlayer = UE::Geometry::Distance(HeightCheck.Location, PawnOwner->GetActorLocation());
//Keep player from going into the ground but don't make them float
if (DistanceBetweenGroundAndPlayer < 110)
{
PawnOwner->SetActorLocation(PawnOwner->GetActorLocation() + CurrentPlaneNormal);
}
else if (Grounded && DistanceBetweenGroundAndPlayer > 120)
{
PawnOwner->SetActorLocation(PawnOwner->GetActorLocation() - CurrentPlaneNormal);
}
FHitResult BlockHit;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AProjectile::StaticClass(), IgnoreActors);
UGameplayStatics::GetAllActorsOfClass(GetWorld(), ABulletShell::StaticClass(), ActorsFound);
IgnoreActors.Append(ActorsFound);
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AWeaponPickupable::StaticClass(), ActorsFound);
IgnoreActors.Append(ActorsFound);
IgnoreActors.Add(PawnOwner);
TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes;
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECollisionChannel::ECC_WorldStatic));
UKismetSystemLibrary::SphereTraceSingleForObjects(
GetWorld(),
PawnOwner->GetActorLocation() + PawnOwner->GetActorUpVector()*25 + PawnOwner->GetActorForwardVector()*25,
PawnOwner->GetActorLocation() + PawnOwner->GetActorUpVector()*35 + PawnOwner->GetActorForwardVector()*100,
50.f,
ObjectTypes,
false,
IgnoreActors,
EDrawDebugTrace::None, //change to EDrawDebugTrace::ForOneFrame for debug draw
BlockHit,
true);
//Check if you've hit a wall in front of you
if (!BlockHit.bBlockingHit || BlockHit.GetHitObjectHandle().GetRepresentedClass()->GetName().Contains("BP_HealthOrb") || BlockHit.GetHitObjectHandle().GetRepresentedClass()->GetName().Contains("BP_ProjectileWeapon")/*temporary fix*/)
{
// Apply substepping for physics calculations
constexpr int MaxSubstepsPerSecond = 20;
constexpr float MaxSubstepDeltaTime = 1.0f / MaxSubstepsPerSecond;
const int SubstepCount = FMath::CeilToInt(GetWorld()->GetDeltaSeconds() / MaxSubstepDeltaTime);
constexpr int MaxSubsteps = 20; // Adjust as needed
const int NumSubsteps = FMath::Clamp(SubstepCount, 1, MaxSubsteps);
const float SubstepDeltaTime = GetWorld()->GetDeltaSeconds() / NumSubsteps;
Velocity = FVector::ZeroVector;
for (int SubstepIndex = 0; SubstepIndex < NumSubsteps; ++SubstepIndex)
{
// Apply gravity if not grounded
if (!Grounded)
{
ExtraMovement.Z += Gravity * 75 * SubstepDeltaTime;
}
// Calculate movement for this substep
FVector Movement = (Direction * Speed + ExtraMovement) * SubstepDeltaTime;
// Accumulate movement for this substep
Velocity += Movement;
// Perform physics calculations for this substep
FHitResult MovementHit;
bool Moved = SafeMoveUpdatedComponent(Velocity, PawnOwner->GetActorForwardVector().ToOrientationQuat(), true, MovementHit);
if (!Moved)
{
Direction+=CurrentPlaneNormal*10;
PawnOwner->SetActorLocation(PawnOwner->GetActorLocation() + CurrentPlaneNormal*10);
}
}
float MovementPitch = GetAngleBetweenVectors(Direction, FVector(0,0,1));
if (MovementPitch == 0.f)// if vertical
{
Direction = FVector(0,0,1);
}
if (Grounded)
{
// Uphill slowdown
if (MovementPitch < 70.f)// if too steep
{
if (Speed > 0)
{
Speed -= 2000 * GetWorld()->GetDeltaSeconds();
}
else
{
PawnOwner->SetActorRotation(FRotator(PawnOwner->GetActorRotation().Pitch, PawnOwner->GetActorRotation().Yaw + 180, PawnOwner->GetActorRotation().Roll));
}
}
// Sideways slope forced turning
float MovementRoll = GetAngleBetweenVectors(Direction.Cross(CurrentPlaneNormal),PawnOwner->GetActorUpVector());
if (MovementRoll < 45.f)
{
float RotationAmount = FMath::Clamp(1/(MovementRoll*0.1f), 0, 1);
PawnOwner->SetActorRotation(FRotator(PawnOwner->GetActorRotation().Pitch, PawnOwner->GetActorRotation().Yaw + RotationAmount,
PawnOwner->GetActorRotation().Roll));
}
else if (MovementRoll > 135.f)
{
float RotationAmount = FMath::Clamp(1/(FMath::Abs(MovementRoll-180)*0.1f), 0, 1);
PawnOwner->SetActorRotation(FRotator(PawnOwner->GetActorRotation().Pitch, PawnOwner->GetActorRotation().Yaw - RotationAmount,
PawnOwner->GetActorRotation().Roll));
}
}
}
else // hit wall
{
Speed = 1000;
FVector VectorPerpendicularToWall = BlockHit.ImpactNormal.Cross(PawnOwner->GetActorUpVector());
float DistanceToRight = GetAngleBetweenVectors(Direction, VectorPerpendicularToWall);
float DistanceToLeft = GetAngleBetweenVectors(Direction, -VectorPerpendicularToWall);
if (DistanceToLeft >= DistanceToRight)
{
PawnOwner->SetActorRotation(FRotator(PawnOwner->GetActorRotation().Pitch, (UKismetMathLibrary::FindLookAtRotation(BlockHit.Location,
BlockHit.Location+VectorPerpendicularToWall) + FRotator(0,5,0)).Yaw, PawnOwner->GetActorRotation().Roll));
PawnOwner->SetActorLocation(PawnOwner->GetActorLocation() + PawnOwner->GetActorRightVector()*10);
}
else
{
PawnOwner->SetActorRotation(FRotator(PawnOwner->GetActorRotation().Pitch, (UKismetMathLibrary::FindLookAtRotation(BlockHit.Location, BlockHit.Location
VectorPerpendicularToWall) - FRotator(0,5,0)).Yaw, PawnOwner->GetActorRotation().Roll));
PawnOwner->SetActorLocation(PawnOwner->GetActorLocation() + PawnOwner->GetActorRightVector()*-10);
}
if (!Grounded)
{
Direction = PawnOwner->GetActorForwardVector();
}
}
if (CoyoteFrames.Num() < NumOfCoyoteFrames)
{
CoyoteFrames.Add(Grounded);
}
else
{
CoyoteFrames.RemoveAt(0);
CoyoteFrames.Add(Grounded);
}
PreviousNormalPlane = CurrentPlaneNormal;
PreviousHits = Hits;
PreviousFrameGrounded = Grounded;
}
This code has gone through the most iteration out of anything in the project since it is one of the main core features. Many playtests, both internal and external, have dictated changes that needed to be done with the snowboard. I am quite satisfied with the result but there are a couple parts I would have liked to rework to implement better solutions. In the end, I believe I did very well for the time I had and I am proud of this work.