Как-то раз, потребовалось написать модуль для скрытия объектов между камерой и персонажем, либо между несколькими персонажами для RTS игры. Хочу поделиться для тех, кто начал свой путь в Unreal Engine. Данный туториал, если его можно так назвать, будет с использованием С++, но в прилагаемом проекте на github будет вариант и на Blueprint, функционал обоих идентичен.
Видео пример
И так, поехали. Разобьем нашу задачу на несколько мелких:
- Получить объекты между камерой и персонажем.
- Изменить материал этих объектов на нужный.
- Изменить материал обратно на тот что был, если объект не мешает обзору нашего персонажа.
Нам потребуются 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);
Возможно кому-то будет полезным. Или кто-то расскажет свое мнение в комментариях.
Ссылка на исходники
ufna
Я бы назвал ваше решение несколько, гм, не оптимальным. Или даже примером «как делать не надо». Даже простой трейс из камеры вперёд был бы на порядок (или два) легче, давая в общем-то тот же самый результат. Зачем вы перебираете объекты в принципе? :(
Deus-Ex-Machina Автор
Я вас не совсем понял, что значит простой трейс, чем он отличается от моего простого трейса? Мне же нужно поменять материал на всех объектах, для этого я их и перебираю.
ufna
1. GetAllActorsOfClass стоит воспринимать как табу для всего, что имеет хоть какой-то период, отличный от «пару раз за игру».
2. Прежде чем кидать дорогие большие капсульные трейсы, было бы неплохо определить, а персонаж вообще есть на экране в теории?
3. Кидание капсулы мало того, что дорогое, но и будет давать артефакты по скрытию предметов близко к камере, что в случае, например, FPS, будет печальным. Может стоило обойтись трейсами в некие реперные точки?
P.S. — если вы пишете под анриал, использовать не-ue4 кодестайл — призрак дурного тона.
Deus-Ex-Machina Автор
1. Да действительно, можно было бы и один раз использовать, Вы правы.
2. Да можно, но в моей ситуации он всегда на экране, это не универсальный код покрывающий любые возможные ситуации во всех проектах. Только в моем.
3. Один трейс капсулой ИМХО лучший вариант.
P.S. Извините, что это Вас так задело. :-)