FadeObjects - Hide objects between camera and character

image

Once, it was necessary to write a module to hide objects between the camera and the character, or between several characters for the RTS game. I want to share for those who started their journey in the Unreal Engine. This tutorial, if you can call it that, will be using C ++, but in the attached project on github there will be an option on Blueprint, the functionality of both is identical.

Video example


And so, let's go. We divide our task into several smaller ones:

  1. Get the objects between the camera and the character.
  2. Change the material of these objects to the desired.
  3. Change the material back to the one that was, if the object does not interfere with the review of our character.

We will need 2 timers, one adds objects to the array to work with them, and the second to change the object itself, in this case I change the material from normal to slightly transparent. This material can be replaced by any suitable for you.

SFadeObjectsComponent.h

FTimerHandle timerHandle_ObjectComputeTimer;
FTimerHandle timerHandle_AddObjectsTimer;

As soon as the object is in the array, for further work we need to memorize some of its properties, for example, what material it had before we changed it, because we will have to change it back. Also, in our case, we hide, and when necessary we return the original state of the object gradually, so we need to remember its current state.

To do this, we will create a structure:
USTRUCT()
structFFadeObjStruct
{
	GENERATED_USTRUCT_BODY()
	UPROPERTY()
	UPrimitiveComponent* primitiveComp;
	UPROPERTY()
	TArray<UMaterialInterface*> baseMatInterface;
	UPROPERTY()
	TArray<UMaterialInstanceDynamic*> fadeMID;
	UPROPERTY()
	float fadeCurrent;
	UPROPERTY()
	bool bToHide;
	voidNewElement(UPrimitiveComponent* newComponent, TArray<UMaterialInterface*> newBaseMat, <UMaterialInstanceDynamic*> newMID, float currentFade, bool bHide){
    	primitiveComp = newComponent;
    	baseMatInterface = newBaseMat;
    	fadeMID = newMID;
    	fadeCurrent = currentFade;
    	bToHide = bHide;
	}
	voidSetHideOnly(bool hide){
    	bToHide = hide;
	}
	voidSetFadeAndHide(float newFade, bool newHide){
    	fadeCurrent = newFade;
    	bToHide = newHide;
	}
	//For DestroyvoidDestroy(){
    	primitiveComp = nullptr;
	}
	//Constructor
	FFadeObjStruct()
	{
    	primitiveComp = nullptr;
    	fadeCurrent = 0;
    	bToHide = true;
	}
};


We also need some of the settings available from Blueprint for the flexible operation of our component. Such as the type of collision to identify objects, the size of the capsule (the beam itself) from character to camera, the larger the size, the more objects around the character will be captured.

// Check trace block by this
	UPROPERTY(EditAnywhere, Category = "Fade Objects")
	TArray<TEnumAsByte<ECollisionChannel>> objectTypes;
 // Trace object size
	UPROPERTY(EditAnywhere, Category = "Fade Objects")
	float capsuleHalfHeight;
	// Trace object size
	UPROPERTY(EditAnywhere, Category = "Fade Objects")
	float capsuleRadius;

The distance at which objects will be hidden.

UPROPERTY(EditAnywhere, Category = "Fade Objects")
float workDistance;

And of course, the character class itself or other actors in the scene.

UPROPERTY(EditAnywhere, Category = "Fade Objects")
UClass* playerClass;

We will not parse all the variables used, you can independently get acquainted with the source code.

Let's turn to implementation. In BeginPlay we will start our timers. Instead of timers, you can, of course, use EventTick, but it is better not to do this, the operation itself for changing materials if a large number of objects is quite expensive for the CPU.

SFadeObjectsComponent.cpp

GetWorld()->GetTimerManager().SetTimer(timerHandle_AddObjectsTimer, this, &USFadeObjectsComponent::AddObjectsToHide, addObjectInterval, true); 
GetWorld()->GetTimerManager().SetTimer(timerHandle_ObjectComputeTimer, this, &USFadeObjectsComponent::FadeObjWorker, calcFadeInterval, true);                             

The function of adding an object to an array. Here I would like to note that she adds not only the actor in the scene, but also its components and SkeletalMesh, if necessary.
void USFadeObjectsComponent::AddObjectsToHide()
{
	UGameplayStatics::GetAllActorsOfClass(this, playerClass, characterArray);
	for (AActor* currentActor : characterArray)
	{
		const FVector traceStart = GEngine->GetFirstLocalPlayerController(GetWorld())->PlayerCameraManager->GetCameraLocation();
		const FVector traceEnd = currentActor->GetActorLocation();
		const FRotator traceRot = currentActor->GetActorRotation();
		FVector traceLentgh = traceStart - traceEnd;
		const FQuat acQuat = currentActor->GetActorQuat();
		if (traceLentgh.Size() < workDistance)
		{
			FCollisionQueryParams traceParams(TEXT("FadeObjectsTrace"), true, GetOwner());
			traceParams.AddIgnoredActors(actorsIgnore);
			traceParams.bTraceAsyncScene = true;
			traceParams.bReturnPhysicalMaterial = false;
			// Not tracing complex uses the rough collision instead making tiny objects easier to select.
			traceParams.bTraceComplex = false;
			TArray<FHitResult> hitArray;
			TArray<TEnumAsByte<EObjectTypeQuery>> traceObjectTypes;
			// Convert ECollisionChannel to ObjectTypefor (int i = 0; i < objectTypes.Num(); ++i)
			{
				traceObjectTypes.Add(UEngineTypes::ConvertToObjectType(objectTypes[i].GetValue()));
			}
			// Check distance between camera and player for new object to fade, and add this in array
			GetWorld()->SweepMultiByObjectType(hitArray, traceStart, traceEnd, acQuat, traceObjectTypes,
				FCollisionShape::MakeCapsule(capsuleRadius, capsuleHalfHeight), traceParams);
			for (int hA = 0; hA < hitArray.Num(); ++hA)
			{
				if (hitArray[hA].bBlockingHit && IsValid(hitArray[hA].GetComponent()) && !fadeObjectsHit.Contains(hitArray[hA].GetComponent()))
				{
					fadeObjectsHit.AddUnique(hitArray[hA].GetComponent());
				}
			}
		}
	}
	// Make fade array after complete GetAllActorsOfClass loopfor (int fO = 0; fO < fadeObjectsHit.Num(); ++fO)
	{
		// If not contains this component in fadeObjectsTempif (!fadeObjectsTemp.Contains(fadeObjectsHit[fO]))
		{
			TArray<UMaterialInterface*> lBaseMaterials;
			TArray<UMaterialInstanceDynamic*> lMidMaterials;
			lBaseMaterials.Empty();
			lMidMaterials.Empty();
			fadeObjectsTemp.AddUnique(fadeObjectsHit[fO]);
			// For loop all materials ID in objectfor (int nM = 0; nM < fadeObjectsHit[fO]->GetNumMaterials(); ++nM)
			{
				lMidMaterials.Add(UMaterialInstanceDynamic::Create(fadeMaterial, fadeObjectsHit[fO]));
				lBaseMaterials.Add(fadeObjectsHit[fO]->GetMaterial(nM));
				// Set new material on object
				fadeObjectsHit[fO]->SetMaterial(nM, lMidMaterials.Last());
			}
			// Create new fade object in array of objects to fade
			FFadeObjStruct newObject;
			newObject.NewElement(fadeObjectsHit[fO], lBaseMaterials, lMidMaterials, immediatelyFade, true);
			// Add object to array
			fadeObjects.Add(newObject);
			// Set collision on Primitive Component
			fadeObjectsHit[fO]->SetCollisionResponseToChannel(ECC_Camera, ECR_Ignore);
		}
	}
	// Set hide to visible true if containsfor (int fOT = 0; fOT < fadeObjectsTemp.Num(); ++fOT)
	{
		if (!fadeObjectsHit.Contains(fadeObjectsTemp[fOT]))
		{
			fadeObjects[fOT].SetHideOnly(false);
		}
	}
	// Clear array
	fadeObjectsHit.Empty();
}


The function for working with objects that changes the material from the original to the required and back.
void USFadeObjectsComponent::FadeObjWorker()
{
	if (fadeObjects.Num() > 0)
	{
    	// For loop all fade objectsfor (int i = 0; i < fadeObjects.Num(); ++i)
    	{
        	// Index of iterationint fnID = i;
        	float adaptiveFade;
        	if (fnID == fadeObjects.Num())
        	{
            	adaptiveFade = nearObjectFade;
        	}
        	else
        	{
            	adaptiveFade = farObjectFade;
        	}
        	// For loop fadeMID arrayfor (int t = 0; t < fadeObjects[i].fadeMID.Num(); ++t)
        	{
            	float targetF;
            	constfloat currentF = fadeObjects[i].fadeCurrent;
            	if (fadeObjects[i].bToHide)
            	{
                	targetF = adaptiveFade;
            	}
            	else
            	{
                	targetF = 1.0f;
            	}
            	constfloat newFade = FMath::FInterpConstantTo(currentF, targetF, GetWorld()->GetDeltaSeconds(), fadeRate);
            	fadeObjects[i].fadeMID[t]->SetScalarParameterValue("Fade", newFade);
            	currentFade = newFade;
            	fadeObjects[i].SetFadeAndHide(newFade, fadeObjects[i].bToHide);
        	}
        	// remove index in arrayif (currentFade == 1.0f)
        	{
            	for (int bmi = 0; bmi < fadeObjects[fnID].baseMatInterface.Num(); ++bmi)
            	{
                	fadeObjects[fnID].primitiveComp->SetMaterial(bmi, fadeObjects[fnID].baseMatInterface[bmi]);
            	}
            	fadeObjects[fnID].primitiveComp->SetCollisionResponseToChannel(ECC_Camera, ECR_Block);
            	fadeObjects.RemoveAt(fnID);
            	fadeObjectsTemp.RemoveAt(fnID);
        	}
    	}
	}
}


There is nothing special to tell here, some pieces of code, and so with comments. The video at the beginning shows the result. I also want to add only the settings with which the component is initialized.

PrimaryComponentTick.bCanEverTick = false;
bEnable = true;
addObjectInterval = 0.1f;
calcFadeInterval = 0.05f;
fadeRate = 10.0f;
capsuleHalfHeight = 88.0f;
capsuleRadius = 34.0f;
workDistance = 5000.0f;
nearCameraRadius = 300.0f;
nearObjectFade = 0.3;
farObjectFade = 0.1;
immediatelyFade = 0.5f;
// Add first collision type
objectTypes.Add(ECC_WorldStatic);

Perhaps someone will be useful. Or someone will tell their opinion in the comments.

Link to source

Also popular now: