Как стать автором
Обновить

FadeObjects — Скрываем объекты между камерой и персонажем

Время на прочтение6 мин
Количество просмотров6.7K
image

Как-то раз, потребовалось написать модуль для скрытия объектов между камерой и персонажем, либо между несколькими персонажами для RTS игры. Хочу поделиться для тех, кто начал свой путь в Unreal Engine. Данный туториал, если его можно так назвать, будет с использованием С++, но в прилагаемом проекте на github будет вариант и на Blueprint, функционал обоих идентичен.

Видео пример


И так, поехали. Разобьем нашу задачу на несколько мелких:

  1. Получить объекты между камерой и персонажем.
  2. Изменить материал этих объектов на нужный.
  3. Изменить материал обратно на тот что был, если объект не мешает обзору нашего персонажа.

Нам потребуются 2 таймера, один добавляет объекты в массив для работы с ними, и второй для изменения самого объекта, в данном случае я меняю материал с обычного на слегка прозрачный. Этот материал Вы можете заменить на любой подходящий для вас.

SFadeObjectsComponent.h

FTimerHandle timerHandle_ObjectComputeTimer;

FTimerHandle timerHandle_AddObjectsTimer;

Как только объект оказался в массиве, для дальнейшей работы нам нужно запоминать некоторые его свойства, например, какой материал у него был до того как мы его изменили, ведь нам придется менять его обратно. Также, в нашем случае мы скрываем, а когда необходимо возвращаем изначальное состояние объекта постепенно, поэтому потребуется помнить его текущее состояние.

Для этого мы создадим структуру:
USTRUCT()
struct FFadeObjStruct
{
	GENERATED_USTRUCT_BODY()
 
	UPROPERTY()
	UPrimitiveComponent* primitiveComp;
 
	UPROPERTY()
	TArray<UMaterialInterface*> baseMatInterface;
 
	UPROPERTY()
	TArray<UMaterialInstanceDynamic*> fadeMID;
 
	UPROPERTY()
	float fadeCurrent;
 
	UPROPERTY()
	bool bToHide;
 
	void NewElement(UPrimitiveComponent* newComponent, TArray<UMaterialInterface*> newBaseMat, <UMaterialInstanceDynamic*> newMID, float currentFade, bool bHide)
	{
    	primitiveComp = newComponent;
    	baseMatInterface = newBaseMat;
    	fadeMID = newMID;
    	fadeCurrent = currentFade;
    	bToHide = bHide;
	}
 
	void SetHideOnly(bool hide)
	{
    	bToHide = hide;
	}
 
	void SetFadeAndHide(float newFade, bool newHide)
	{
    	fadeCurrent = newFade;
    	bToHide = newHide;
	}
 
	//For Destroy
	void Destroy()
	{
    	primitiveComp = nullptr;
	}
 
	//Constructor
	FFadeObjStruct()
	{
    	primitiveComp = nullptr;
    	fadeCurrent = 0;
    	bToHide = true;
	}
};


Еще нам потребуются некоторые настройки доступные из Blueprint для гибкой работы нашего компонента. Такие, как тип коллизии для идентификации объектов, размер капсулы (самого луча) от персонажа до камеры, чем больше размер, тем больше объектов вокруг персонажа будет захвачено.

// 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;

Дистанция на которой объекты будут скрываться.

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

И конечно же, сам класс персонажа или других актеров в сцене.

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

Не будем разбирать все используемые переменные, вы можете самостоятельно ознакомиться с исходниками.

Перейдем к реализации. В BeginPlay запустим наши таймеры. Вместо таймеров можно, конечно, использовать и EventTick, но лучше этого не делать, сама операция по изменению материалов, если объектов большое количество довольно затратная для CPU.

SFadeObjectsComponent.cpp

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

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

Функция добавления объекта в массив. Тут хотелось бы отметить, что она добавляет не только самого актера в сцене, но и его составляющие и SkeletalMesh, если потребуется.
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 ObjectType
			for (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 loop
	for (int fO = 0; fO < fadeObjectsHit.Num(); ++fO)
	{
		// If not contains this component in fadeObjectsTemp
		if (!fadeObjectsTemp.Contains(fadeObjectsHit[fO]))
		{
			TArray<UMaterialInterface*> lBaseMaterials;
			TArray<UMaterialInstanceDynamic*> lMidMaterials;

			lBaseMaterials.Empty();
			lMidMaterials.Empty();

			fadeObjectsTemp.AddUnique(fadeObjectsHit[fO]);

			// For loop all materials ID in object
			for (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 contains
	for (int fOT = 0; fOT < fadeObjectsTemp.Num(); ++fOT)
	{
		if (!fadeObjectsHit.Contains(fadeObjectsTemp[fOT]))
		{
			fadeObjects[fOT].SetHideOnly(false);
		}
	}

	// Clear array
	fadeObjectsHit.Empty();
}


Функция для работы с объектами меняющая материал с изначального на требуемый и обратно.

void USFadeObjectsComponent::FadeObjWorker()
{
	if (fadeObjects.Num() > 0)
	{
    	// For loop all fade objects
    	for (int i = 0; i < fadeObjects.Num(); ++i)
    	{
        	// Index of iteration
        	int fnID = i;
 
        	float adaptiveFade;
 
        	if (fnID == fadeObjects.Num())
        	{
            	adaptiveFade = nearObjectFade;
        	}
        	else
        	{
            	adaptiveFade = farObjectFade;
        	}
 
        	// For loop fadeMID array
        	for (int t = 0; t < fadeObjects[i].fadeMID.Num(); ++t)
        	{
            	float targetF;
 
            	const float currentF = fadeObjects[i].fadeCurrent;
 
            	if (fadeObjects[i].bToHide)
            	{
                	targetF = adaptiveFade;
            	}
            	else
            	{
                	targetF = 1.0f;
            	}
 
            	const float 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 array
        	if (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);
        	}
    	}
	}
}


Рассказывать здесь особо нечего, некоторые куски кода и так с комментариями. Видео в начале демонстрирует результат. Хочу добавить еще лишь настройки, с которыми инициализируется компонент.

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);

Возможно кому-то будет полезным. Или кто-то расскажет свое мнение в комментариях.

Ссылка на исходники
Теги:
Хабы:
Всего голосов 9: ↑6 и ↓3+3
Комментарии4

Публикации

Истории

Работа

Ближайшие события