Основи реалізації чистої архітектури в UE-5 за допомогою C++

Вивчаючи розробку ігор в Unreal Engine на C++ не так давно, я натрапив на проблему в проєкті, над яким працював. Мої класи ставали надто заплутаними, з великою кількістю методів та умов if else.

Тому я вирішив реалізувати більш чистий код, орієнтуючись на принципи Clean Architecture.

Я трохи пошукав в інтернеті, але не знайшов багато прикладів практичного використання Clean Architecture в Unreal Engine з використанням мови C++.

Сподіваюся, цей артикул буде корисним для інших розробників (devs) і стане відправною точкою.

Структура проєкту

pic

У межах Application ми маємо BallStaticMeshActor, що представляє футбольний м'яч, і ми хочемо маніпулювати його траєкторією згідно з введенням, яке ми отримуємо від нашого гравця.

Для цього ми створимо компонент, відповідальний за малювання та маніпулювання траєкторією м'яча.
Ми створили файл з назвою SplineDrawComponent в межах Components/StaticMeshActor, де зберігаються всі компоненти, які можуть маніпулювати класами типу AStaticMeshActor.

// USplineDrawComponent.h  

class NEWPROJECT_API USplineDrawComponent : public UActorComponent, public ISplineDrawComponentInterface  
{  
 GENERATED_BODY()  

public:  
 USplineDrawComponent();  

 virtual void SetDistance(float Value) override;  
 virtual void SetCurrentDistance(float Value) override;  
 virtual void SetComponentVelocity(FVector Velocity) override;  
 virtual void AddSplinePointFunction() override;  
 virtual float GetDistance() override;  
 virtual float GetCurrentDistance() override;  
 virtual float GetCuurentVelocity() override;  
 virtual float CalculateSplineLength(FVector TrajectoryEnd) override;  

 float CurrentDistance = 0.0f;  
 float Distance = 0.0f;  
};
// USplineDrawComponent.cpp  

USplineDrawComponent::USplineDrawComponent()  
{

PrimaryComponentTick.bCanEverTick = true;  
}  

void USplineDrawComponent::AddSplinePointFunction()  
{  
 if (!GetOwner())  
 {  
 UE_LOG(LogTemp, Warning, TEXT("GetOwner не знайдено в USplineDrawComponent::AddSplinePointFunction"));  
 return;  
 }  

 FVector Start = FVector(GetOwner()->GetActorLocation().X, GetOwner()->GetActorLocation().Y, 24.f);  
 FVector End = Start + Start.GetSafeNormal() * Distance;  

 // Намалюємо траєкторію за допомогою Debug Draw  
 DrawDebugLine(GetWorld(), Start, FVector(End.X, End.Y, 24.f), FColor::Green, true, 1.f, SDPG_World, 4.f);  
 DrawDebugPoint(GetWorld(), Start, 10.f, FColor::Blue, true);  
 DrawDebugPoint(GetWorld(), FVector(End.X, End.Y, 24.f), 10.f, FColor::Blue, true);  
}  

void USplineDrawComponent::SetComponentVelocity(FVector Velocity)  
{  
 AStaticMeshActor* Actor = Cast<GetOwner>();  
 if (!Actor)  
 {  
 UE_LOG(LogTemp, Warning, TEXT("Актор не знайдений в USplineDrawComponent::SetComponentVelocity"));  
 }

return;  
 }  

 Actor->GetStaticMeshComponent()->SetPhysicsLinearVelocity(FVector(Velocity.X, Velocity.Y, 22.5f));  
}  

float USplineDrawComponent::CalculateSplineLength(FVector TrajectoryEnd)  
{  
 AStaticMeshActor* Actor = Cast<GetOwner>();  
 if (!Actor)  
 {  
 UE_LOG(LogTemp, Warning, TEXT("Актор не знайдений в USplineDrawComponent::CalculateSplineLength"));  
 return 0.f;  
 }  

 FVector Start = FVector(GetOwner()->GetActorLocation().X, GetOwner()->GetActorLocation().Y, 0.f);  
 return FVector::Distance(Start, TrajectoryEnd);  
}  

float USplineDrawComponent::GetDistance()  
{  
 return Distance;  
}  

float USplineDrawComponent::GetCurrentDistance()  
{  
 return CurrentDistance;  
}  

float USplineDrawComponent::GetCuurentVelocity()  
{  
 return GetOwner()->GetVelocity().Size2D();  
}  

void USplineDrawComponent::SetCurrentDistance(float Value)  
{  
 CurrentDistance = Value;  
}

void USplineDrawComponent::SetDistance(float Value)  
{  
 Distance = Value;  
}

Зверніть увагу, що цей компонент реалізує інтерфейс ISplineDrawComponentInterface.h

class NEWPROJECT_API ISplineDrawComponentInterface  
{  
 GENERATED_BODY()  

 // Додайте функції інтерфейсу до цього класу.

Це клас, який буде успадкований для реалізації цього інтерфейсу.
public:  
 virtual void SetComponentVelocity(FVector Velocity) = 0;  
 virtual void AddSplinePointFunction() = 0;  

 virtual void SetDistance(float Value) = 0;  
 virtual void SetCurrentDistance(float CurrentDistance) = 0;  

 virtual float GetDistance() = 0;  
 virtual float GetCurrentDistance() = 0;  
 virtual float GetCuurentVelocity() = 0;  
 virtual float CalculateSplineLength(FVector TrajectoryEnd) = 0;  
};

Тепер давайте створимо Використання (Use Case), яке містить логіку, яку ми будемо використовувати для переміщення м'яча за певною траєкторією.

// UTrajectoryRuntimeDrawUseCase.h  

class NEWPROJECT_API UTrajectoryRuntimeDrawUseCase : public UObject  
{  
 GENERATED_BODY()  

public:  
 UTrajectoryRuntimeDrawUseCase();  

 static void Handle  
 (  
 TScriptInterface SplineBallComponentInterface,  
 const FVector& Input,  
 float DeltaTime  
 );  
};
// UTrajectoryRuntimeDrawUseCase.cpp  

void UTrajectoryRuntimeDrawUseCase::Handle  
{

(  
 TScriptInterface SplineBallComponentInterface,  
 const FVector& Input,  
 float DeltaTime  
)  
{  
 // Тут ми реалізуємо логіку траєкторії нашого м'яча  
 float Distance = SplineBallComponentInterface->GetDistance();  
 if (Distance > 0.0f)  
 {  
 float DistanceAt = SplineBallComponentInterface->GetCurrentDistance();  
 float VelocityAt = SplineBallComponentInterface->GetCuurentVelocity();  
 float CurrentDistance = DistanceAt + (VelocityAt * DeltaTime);  

 SplineBallComponentInterface->SetCurrentDistance(CurrentDistance);  
 if (CurrentDistance > Distance)  
 {  
 SplineBallComponentInterface->SetCurrentDistance(Distance);  
 SplineBallComponentInterface->SetComponentVelocity(FVector::ZeroVector);  
 return;  
 }  

 return;  
 }  

 float Average = SplineBallComponentInterface->CalculateSplineLength(Input);  
 SplineBallComponentInterface->SetDistance(Average * 1.4f);  
 SplineBallComponentInterface->AddSplinePointFunction();
// Input * Force   
 FVector LinearVelocity = Input.GetSafeNormal() * 200.0f;  
 SplineBallComponentInterface->SetComponentVelocity(LinearVelocity);  
}

Зверніть увагу, що ми можемо мати декілька Використань (Use Case) для одного компонента і можемо реалізувати цей компонент у різних класах, що успадковують AStaticMeshActor, використовуючи різні Використання (Use Case) або повторно використовувати вже існуюче використання.

Наприкінці давайте використаємо цей компонент та створене використання для визначення траєкторії нашого м'яча.

У Application/BallStaticMeshActor створіть файл з наступним кодом.

// Sets default values  
ABallStaticMeshActor::ABallStaticMeshActor()  
{  

 PrimaryActorTick.bCanEverTick = true;  

 // Ініціалізуємо наш компонент  
 Spline = CreateDefaultSubobject(TEXT("Spline"));  
 Spline->RegisterComponent();  

 static ConstructorHelpers::FObjectFinder BallStaticMesh(TEXT("/Script/Engine.StaticMesh'/Game/SuaStaticMesh'"));  

if (BallStaticMesh.Succeeded())  
 {  
 GetStaticMeshComponent()->SetStaticMesh(BallStaticMesh.Object);  
 GetStaticMeshComponent()->SetMassOverrideInKg(NAME_None, 0.450f);  
 }  

 SetActorScale3D(FVector(1.1f, 1.1f, 1.1f));  
 GetStaticMeshComponent()->SetMobility(EComponentMobility::Movable);  
 GetStaticMeshComponent()->SetSimulatePhysics(true);  
 GetStaticMeshComponent()->SetEnableGravity(true);  
 GetStaticMeshComponent()->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);  
 GetStaticMeshComponent()->SetCollisionResponseToAllChannels(ECR_Block);  

 GetStaticMeshComponent()->SetupAttachment(RootComponent);  
}  


void ABallStaticMeshActor::BeginPlay()  
{  
 Super::BeginPlay();  
}  

void ABallStaticMeshActor::Tick(float DeltaTime)  
{  
 Super::Tick(DeltaTime);  

 if (Spline)  
 {  
 // Тут ми передаємо в наше використання компонент, відповідальний за маніпулювання нашим м'ячем, і фінальну траєкторію м'яча
FVector Trajectory = FVector(GetActorLocation().X * 2, GetActorLocation().Y * 2, 0.f);  
 UTrajectoryRuntimeDrawUseCase::Handle(Spline, Trajectory, DeltaTime);  
 }  
}

З цим ми отримуємо наступний результат.

pic

GitHub з кодами проєкту.

Сподіваюся, що це допомогло, 😊

Перекладено з: Implementação básica clean architecture na UE-5 com c++

Leave a Reply

Your email address will not be published. Required fields are marked *