• Ladders

    Implementation:

    Setup:

    • ALadder : public AActor, public IInteractable
    • We dynamically create the ladder by setting the LadderHeight and then add an instanced mesh and fixing the LadderBox, TopPoint, and BottomPoint.
    // HISM Ladder Mesh
    FTransform InstanceTransform;
    const FVector LadderMeshSize = HISLadder->GetStaticMesh()->GetBoundingBox().GetSize();
    for (int8 i = 0; i < LadderHeight; ++i)
    {
    	InstanceTransform.SetLocation(FVector(0.0, 0.0, LadderMeshSize.Z * i));
    	HISLadder->AddInstance(InstanceTransform);
    }
    
    // Sizing the Ladder Collision Box
    if (LadderBox)
    {
    	LadderMeshHeight = LadderMeshSize.Z * LadderHeight;
    	const double LadderBoxHeight = LadderMeshHeight / 2.0;
    	LadderBox->InitBoxExtent(FVector(LadderMeshSize.X, LadderWidthBuffer, LadderBoxHeight + LadderUpperBuffer));
    	LadderBox->SetRelativeLocation(FVector(0.0, LadderWidthBuffer, LadderBoxHeight + LadderUpperBuffer));
    
    	LadderBoxes->InitBoxExtent(FVector(LadderMeshSize.X, LadderWidthBuffer, 50.0));
    	LadderBoxes->SetRelativeLocation(FVector(0.0, -50.0, LadderMeshHeight + 25.0));
    
    	// Sets the UArrowComponent TopPoint and BottomPoint locations & rotations
    	TopPoint->SetRelativeLocation(FVector(0.0, -50.0, LadderMeshHeight + 15.0));
    	TopPoint->SetRelativeRotation(FRotator(0.0, -90.0, 0.0));
    	BottomPoint->SetRelativeLocation(FVector(0.0, 75.0, 0.0));
    	BottomPoint->SetRelativeRotation(FRotator(0.0, 90.0, 0.0));
    }
    • While on the ladder, we want to limit the character’s ability to look left & right (yaw). This prevents the character from rotating 360 degrees around the ladder.
    if (Character && Character->GetPlayerCameraManager())
    {
    	Character->GetPlayerCameraManager()->ViewYawMax = MaxYaw;
    	Character->GetPlayerCameraManager()->ViewYawMin = MinYaw;
    }

    Interacting:

    • The UInteractableComponent requires an Actor to adhere to the IInteractableInterface which then requires the Actor to implement OnInteract().
    • So, when a APawn interacts with the ALadder, we have to determine what should happen:
      • If the Pawn is already on the ladder, then we dismount.
      • I chose to dismount by “launching” the character off the ladder a bit.
    if (Character)
    {
    	const FVector LaunchDir = BottomPoint->GetForwardVector() * 100.0;
    	Character->LaunchCharacter(LaunchDir, true, false);
    	StopClimbing(Character);
    }
    • If the Pawn is not on the ladder, then we check if the APawn can interact with said ladder, if so we “mount” the ladder.
    • Attaching the Character to the Ladder:
    const double AttachHeight = Hit.ImpactPoint.Z;
    if ((AttachHeight > (BottomPoint->GetComponentLocation().Z + 25.0)) && (AttachHeight < LadderMeshHeight))
    {
    	const FVector AttachPoint = CharacterDistanceFromLadder->GetComponentLocation();
    	const FVector TargetLoc = FVector(AttachPoint.X, AttachPoint.Y, AttachHeight);
    	const FRotator TargetRot = FRotator(0.0, GetActorRotation().Yaw - 90.0, 0.0);
    	Interpolate(Character, TargetLoc, TargetRot, InterpSpeed);
    
    	Character->bUseControllerRotationYaw = false;
    	return true;
    }

    Moving Up & Down:

    • As the character moves up & down the ladder, we want to check if the character is near the TopPoint or BottomPoint.
      • Which is simply done by calculating the distance between the character and the points.
      • If the character is near either point, then we interpolate the character from its current location to the point it’s closest to.
    if (DistFromTopPoint < MAXDISTANCEFROMTOPPOINT)
    {
    	//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Can Dismount to Top"));
    
    	StopClimbing(Character);
    	const FVector LandingPoint = TopPoint->GetComponentLocation();
    	const FVector TargetLoc = FVector(LandingPoint.X, LandingPoint.Y, LandingPoint.Z + Character->GetDefaultHalfHeight());
    	const FRotator TargetRot = TopPoint->GetComponentRotation();
    	// Interpolate up
    	Interpolate(Character, FVector(Character->GetActorLocation().X, Character->GetActorLocation().X, LandingPoint.Z + Character->GetDefaultHalfHeight()), TargetRot, InterpSpeed / 2.0f);
    	// Interpolate over
    	Interpolate(Character, TargetLoc, TargetRot, InterpSpeed / 2.0f);
    
    	return true;
    }
    
    if (DistFromBottomPoint < MAXDISTANCEFROMBOTPOINT)
    {
    	//GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Can Dismount to Bottom"));
    
    	StopClimbing(Character);
    	const FVector LandingPoint = BottomPoint->GetComponentLocation();
    	const FVector TargetLoc = FVector(LandingPoint.X, LandingPoint.Y, LandingPoint.Z + Character->GetDefaultHalfHeight());
    	Interpolate(Character, TargetLoc, Character->GetActorRotation(), InterpSpeed);
    
    	return true;
    }
  • Doors

    Implementation:

    • ADoor : public AActor, public IInteractable
    • All interactables override the OnInteract() function
    void ADoor::OnInteract_Implementation(APawn* Pawn)
    {
    	if (Pawn && !GetIsLocked())
    	{
    		const FVector Impulse = Pawn->GetActorForwardVector();
    		PredictRotation(Impulse);
    	}
    }
    • When a APawn interacts with with or collides with the door, we attempt to Predict where the door should be:
    void ADoor::PredictRotation(const FVector& ImpulseDir)
    {
    	if (!Door) return;
    
    	// Finds what side of the door is being hit.
    	const double angle = FVector::DotProduct(ImpulseDir, Door->GetForwardVector());
    	
    	// Front of door
    	if (angle < 0.0)
    	{
    		//UKismetSystemLibrary::PrintString(this, "Forward", true, false, FLinearColor::Black, 100.0f, "Direction");
    		const float x = -PushForce + CurYaw;
    		TargetDoorYaw = FMath::Max(x, -InteriorSwingDistance);
    	}
    	// Back of door
    	else if (angle > 0.0)
    	{
    		//UKismetSystemLibrary::PrintString(this, "Backward", true, false, FLinearColor::Black, 100.0f, "Direction");
    		const float x = PushForce + CurYaw;
    		TargetDoorYaw = FMath::Min(x, ExteriorSwingDistance);
    	}
    	// Side of door
    	else
    	{
    		return;
    	}
    	CurYaw = TargetDoorYaw;
    
    	bIsRotating = true;
    }
    • Finally, the TargetYaw gets replicated, & all clients have the door interpolate from the CurrentYaw to the TargetYaw.
    DOREPLIFETIME(ADoor, TargetDoorYaw);
  • Glass Breaking

    via RenderTargets

    https://youtu.be/fqTEgB1NkHA

    Glass Breaking via RenderTargets:

    • The AGlass actor creates a RenderTarget per actor, then it acquires the glass Material in which it creates a DynamicMaterialInstance, and finally passes the RenderTarget to it.
    RenderTarget = CreateDefaultSubobject<UTextureRenderTarget2D>(TEXT("RenderTarget"));
    if (RenderTarget)
    {
    	RenderTarget->InitAutoFormat(256, 256);
    	RenderTarget->ClearColor = FLinearColor::Black;
    	RenderTarget->bAutoGenerateMips = false;
    	RenderTarget->AddressX = TA_Clamp;
    	RenderTarget->AddressY = TA_Clamp;
    }
    • AGlass creates the GlassBreakBrush as a MaterialInstanceDynamic in OnBegin().
    // Setup Glass Material Render Target
    GlassMaterial = GlassMesh->CreateDynamicMaterialInstance(0, GlassMesh->GetMaterial(0));
    if (GlassMaterial && RenderTarget)
    {
    	GlassMaterial->SetTextureParameterValue(FName("RenderTarget"), RenderTarget);
    	GlassMesh->SetMaterial(0, GlassMaterial);
    }
    
    // Setup Glass Break Brush
    if (GlassBreakMaterial)
    {
    	GlassBreakBrush = UMaterialInstanceDynamic::Create(GlassBreakMaterial, this);
    }
    • When the AGlass gets hit:
    TRACE_CPUPROFILER_EVENT_SCOPE_STR("Glass-BreakAtLocation");
    if (GlassBreakMaterial && RenderTarget)
    {
    	if (UWorld* World = GetWorld())
    	{
    	  // 1. Find line hit collison location in UV space
    		FVector2D HitColLoc;
    		UGameplayStatics::FindCollisionUV(Hit, 0, HitColLoc);
    
    		// 2. Begin drawing to our Canvas
    		UCanvas* Canvas; FVector2D Size; FDrawToRenderTargetContext Context;
    		UKismetRenderingLibrary::BeginDrawCanvasToRenderTarget(World, RenderTarget, Canvas, Size, Context);
    		// 3.We calculate the brush size, then draw it at given HitColLoc, we randomize the rotation
    		const FVector2D HitSize = (Size * HitColLoc) - (GlassBreakBrushSize / 2.0f);
    		Canvas->K2_DrawMaterial(GlassBreakMaterial, HitSize, FVector2D(GlassBreakBrushSize), FVector2D(0.0),
    								FVector2D::UnitVector, FMath::FRandRange(0.0f, 360.0f));
    		// 4. End drawing to our Canvas
    		UKismetRenderingLibrary::EndDrawCanvasToRenderTarget(World, Context);
    	}
    }

    Notes:

    • The GlassBreak texture is stored as a RGB mask, in which each channel stores a different opacity level.
    • In order to create the illusion of depth, we use 2 copies of the RenderTarget as well as the BumpOffset node.
  • Marker/Ping System

    Implementation:

    • Works by having an APingManager class (derived from AInfo)
      • APingManager – Manages all of the pings for all of the players, by assigning 1 ping to 1 player (1:1).
    • The AGameMode class creates APingManager
    • In order to communicate to the APingManager class, any Actor that wishes to ‘ping’ a location, must implement UPingComponent.

    Why This Implementation?

    • If the ‘pings’ were just handled with an UActorComponent, and any actor we want to ping implements said component, then when the APlayerController possess Pawn1 and pings, a ping will appear. If said APlayController possess Pawn2 and pings, then there would be 2 pings for 1 PlayerController on screen. I did not want this.
    • Why not have APlayerController implement the UPingComponent?
      • Well, APlayerController only exists on the Server & the owning client. So, if the APlayerController pings, then no other client will receive said ping.
    • How about APlayerState?
      • Well, while adding the UPingComponent to the APlayerState will work, since APlayerState exists on all clients as well as the server. The issue is, the NetUpdateFrequency for APlayerState is very low, thus when pinging, the ping will take a noticeable amount of time to appear.