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

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

Ссылка на исходники

Комментарии (4)


  1. ufna
    06.10.2018 23:58

    Я бы назвал ваше решение несколько, гм, не оптимальным. Или даже примером «как делать не надо». Даже простой трейс из камеры вперёд был бы на порядок (или два) легче, давая в общем-то тот же самый результат. Зачем вы перебираете объекты в принципе? :(


    1. Deus-Ex-Machina Автор
      07.10.2018 09:07

      Я вас не совсем понял, что значит простой трейс, чем он отличается от моего простого трейса? Мне же нужно поменять материал на всех объектах, для этого я их и перебираю.


      1. ufna
        07.10.2018 11:39

        1. GetAllActorsOfClass стоит воспринимать как табу для всего, что имеет хоть какой-то период, отличный от «пару раз за игру».
        2. Прежде чем кидать дорогие большие капсульные трейсы, было бы неплохо определить, а персонаж вообще есть на экране в теории?
        3. Кидание капсулы мало того, что дорогое, но и будет давать артефакты по скрытию предметов близко к камере, что в случае, например, FPS, будет печальным. Может стоило обойтись трейсами в некие реперные точки?

        P.S. — если вы пишете под анриал, использовать не-ue4 кодестайл — призрак дурного тона.


        1. Deus-Ex-Machina Автор
          07.10.2018 14:29

          1. Да действительно, можно было бы и один раз использовать, Вы правы.
          2. Да можно, но в моей ситуации он всегда на экране, это не универсальный код покрывающий любые возможные ситуации во всех проектах. Только в моем.
          3. Один трейс капсулой ИМХО лучший вариант.

          P.S. Извините, что это Вас так задело. :-)