Last Summer technical systems

Custom Climbing

The Climbing is handled via a Custom Movement Mode, and Custom Movement Component. To climb, the player must be facing a wall, with valid 'Handholds'/'Climb Points' within a specific radius. The wall and handholds are detected via set heuristic checks that have been playtested extensively by myself and a small group of playtesters.

Once the Player is climbing, to further climb they must to continue detect hand holds, otherwise their climbing will be restricted, they must also manage their stamina, rope length, rope tension and pitons.

These management aspects make climbing a challenge and a problem to solve, similarly to how climbing is in real life. This was my goal with this system, to attempt to emulate a climbers thoughts and behaviour within the player.

Custom Climbing Flowchart

Custom Climbing Code Snippets

Below is the two most important functions dealing with the Climbing, Finding the climb locations such as Handholds and footholds, and Processing the surface data to ensure rotation and location is handled correctly.

void UCustomMovementComponent::FindClimbLocation()

{

  

   const FVector StartOffset = UpdatedComponent->GetUpVector() * 0.f;

   const FVector ClimbLocationStart = StartOffset + UpdatedComponent->GetComponentLocation();

   const FVector ClimbLocationEnd = ClimbLocationStart + MovementDirection * ClimbLengthDistance + UpdatedComponent->GetForwardVector() * 60;

   TArray<FHitResult> GrabPointHits = DoCapsuleTraceMultiByObject(ClimbLocationStart, ClimbLocationEnd, CheckForHandHoldRadius,

      CheckForHandHoldRadius, IKTraceTypes, CheckForGrabPoint);

   FVector playerPos = ClimbLocationEnd;

   if (NearestHit != FVector::Zero())

   {

      float DistanceSquared = FVector::Distance(playerPos, NearestHit);

      NearestDistanceSquared = DistanceSquared; 

   }

   if (GrabPointHits.Num() > 0)

   {

      for (const FHitResult& Hit : GrabPointHits)

      {

            // Retrieve the position of the foliage instance

            FVector FoliageInstancePosition = Hit.GetActor()->GetActorLocation();

            float Distance = FVector::Distance(playerPos, FoliageInstancePosition);

            float DistanceToPlayer = FVector::Distance(UpdatedComponent->GetComponentLocation(), FoliageInstancePosition);

           

            // Now you have the position of the foliage instance

            // You can use FoliageInstancePosition for further processing

            if (DistanceToPlayer < ClimbDistanceHardCutoff)

            {

               if (Distance < NearestDistanceSquared)

               {

                  NearestHitResult = Hit;

                  NearestHit = FoliageInstancePosition;

                  NearestDistanceSquared = Distance;

               } 

            }

        

      }

   }


   // Draw debug box for player position

   if (CheckForGrabPoint)

   {

      FBox PlayerDebugBox(playerPos - FVector(10), playerPos + FVector(10));

      DrawDebugBox(GetWorld(), PlayerDebugBox.GetCenter(), PlayerDebugBox.GetExtent(), FColor::Red, false, -1, 0, 2);

      // Draw debug box around the closest point

      FBox ClosestDebugBox(NearestHit - FVector(25), NearestHit + FVector(25));

      DrawDebugBox(GetWorld(), ClosestDebugBox.GetCenter(), ClosestDebugBox.GetExtent(), FColor::Purple, false, -1, 0, 2);  

   }

   // Check if the player has reached the previous closest hit point

}
void UCustomMovementComponent::ProcessClimbableSurfaceInfo()

{

   CurrentClimbableSurfaceLocation = FVector::ZeroVector;

   CurrentClimbableSurfaceNormal = FVector::ZeroVector;

   if (ClimbableSurfacesTracedResults.IsEmpty())

   {

      return;

   }

   const FVector Start = UpdatedComponent->GetComponentLocation();

   const FCollisionShape CollisionSphere = FCollisionShape::MakeSphere(6);

   for (const FHitResult& WallHit : ClimbableSurfacesTracedResults)

   {

      const FVector EndHelpPoint = Start + (WallHit.ImpactPoint - Start).GetSafeNormal() * 120;

      FHitResult AssistHit;

      GetWorld()->SweepSingleByObjectType(AssistHit, Start, EndHelpPoint, FQuat::Identity,

                                       ECC_WorldStatic, CollisionSphere, ClimbQueryParams);

      CurrentClimbableSurfaceLocation += AssistHit.ImpactPoint;

      CurrentClimbableSurfaceNormal += AssistHit.Normal;

   }

   CurrentClimbableSurfaceLocation /= ClimbableSurfacesTracedResults.Num();

   CurrentClimbableSurfaceNormal = CurrentClimbableSurfaceNormal.GetSafeNormal();

   if (ShowDebug)

   {

      Debug::Print(TEXT("Current Surface: ") + CurrentClimbableSurfaceLocation.ToCompactString(), 1, 0,

                   FColor::Green);

   }

   if (ShowDebug)

   {

      Debug::Print(TEXT("Current Normal: ") + CurrentClimbableSurfaceNormal.ToCompactString(), 2, 0, FColor::Purple);

   }

}