Quick tutorial for beginners how to draw lines in user widget using native paint function in C++.
Let’s start with creating NativePaint:
public:
virtual int32 NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
int32 UMyUserWidget::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
FPaintContext Context = FPaintContext(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
// this seems to work too
// FPaintContext Context(AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
//... add loop here
// and
// UWidgetBlueprintLibrary::DrawLine(Context, PointA, PointB, Tint, true, Thickness);
Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);
return LayerId;
}
This is how it looks like in UserWidget.cpp:
Extras
C++ loops are so much faster then Blueprint loops, they can run between milliseconds & picoseconds if they are done right, so it’s worth doing entire widget in C++.
To go even further on optimization create a structure array containing Point A & Point B’s and fill the array in async task.
Structure:
USTRUCT(BlueprintType)
struct FExampleLineDraw{
GENERATED_BODY()
public:
FExampleLineDraw()
: PointA(FVector2D()), PointB(FVector2D()
{}
UPROPERTY(BlueprintReadWrite)
FVector2D PointA
UPROPERTY(BlueprintReadWrite)
FVector2D PointB
};
And then:
public:
UPROPERTY(BlueprintReadOnly)
TArray<FExampleLineDraw> MyArray;
And then the async and all other stuff:
Boilerplate for AsyncTask.
AsyncTask(ENamedThreads::AnyHiPriThreadNormalTask, []()
{
//...
// if you use [this] you don't need this:
AsyncTask(ENamedThreads::GameThread, []()
{
//...
});
});
void UMyPlayerWidget::AsyncWorker()
{
UWorld* world = GetWorld();
if (!::IsValid(world)) return;
AsyncAvailible = false;
ScannerTimer = 0.0f;
TArray<AActor*> FoundActors;
// #include "Kismet/GameplayStatics.h"
// this only works in gamethread
UGameplayStatics::GetAllActorsOfClass(world, AMyActor::StaticClass(), FoundActors);
AsyncTask(ENamedThreads::AnyHiPriThreadNormalTask, [this, FoundActors]()
{
TArray<FExampleLineDraw> tmparray;
APlayerController* PlayerController = this->GetOwningPlayer();
FExampleLineDraw tmpstruct;
for(const AActor* Item : FoundActors)
{
// do stuff here
// #include "Blueprint/WidgetLayoutLibrary.h"
UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition(PlayerController, Item->GetActorLocation(), tmpstruct.PointA, true);
UWidgetLayoutLibrary::ProjectWorldLocationToWidgetPosition(PlayerController, Item->Mesh->GetCenterOfMass(), tmpstruct.PointB, true);
tmparray.Add(tmpstruct)
}
AsyncTask(ENamedThreads::GameThread, [this, tmparray]()
{
this->MyArray = tmparray;
this->AsyncAvailible = true;
});
});
}
void UMyPlayerWidget::Scan()
{
if (!AsyncAvailible) return;
AsyncWorker();
}
// .h file: virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
void UMyPlayerWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
{
Super::NativeTick(MyGeometry, InDeltaTime);
Scan();
if(AsyncAvailible) ScannerTimer += InDeltaTime; // measure process time
}
And then go to native paint:
for(const FExampleLineDraw Item : MyArray)
{
UWidgetBlueprintLibrary::DrawLine(Context, Item.PointA, Item.PointB, FLinearColor::White, true, 0.5f);
}
More about multi-threading: