How to make your own Factory Tick ish group

This might be valuable for anyone needing to make their own factory tick ish thing. Yes I know I am not following some C++ / Unreal style guides. I've been writing javascript for way to many years and your standards are ick to me lol. You should look at using Factory Tick first. This is for special situations where what you want to do does not fall into the patterns of Factory Tick like moving items between inventories without using belts. The Subsystem holds a list of everything we want to tick in this group and calls tick on them.
C++
void AAIO_Group::BeginPlay() {
Super::BeginPlay();
const auto Delay = FMath::FRandRange(.0f, 1.0f);
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AAIO_Group::RunAioTick, 1.0f, true, Delay);
}

void AAIO_Group::EndPlay(const EEndPlayReason::Type EndPlayReason) {
Super::EndPlay(EndPlayReason);
GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
}

void AAIO_Group::RunAioTick() {
const auto Workers = 8
const auto Num = Members.Num();
if (Num == 0) {
return;
}
ParallelFor(Workers, [this, Num](int32 ThreadIndex) {
int32 StartIndex = (Num * ThreadIndex) / Workers;
int32 EndIndex = (Num * (ThreadIndex + 1)) / Workers;
for (int32 Index = StartIndex; Index < EndIndex; ++Index) {
if (IsValid(Members[Index])) {
Members[Index]->AioTick();
}
}
}, false);
}
C++
void AAIO_Group::BeginPlay() {
Super::BeginPlay();
const auto Delay = FMath::FRandRange(.0f, 1.0f);
GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &AAIO_Group::RunAioTick, 1.0f, true, Delay);
}

void AAIO_Group::EndPlay(const EEndPlayReason::Type EndPlayReason) {
Super::EndPlay(EndPlayReason);
GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
}

void AAIO_Group::RunAioTick() {
const auto Workers = 8
const auto Num = Members.Num();
if (Num == 0) {
return;
}
ParallelFor(Workers, [this, Num](int32 ThreadIndex) {
int32 StartIndex = (Num * ThreadIndex) / Workers;
int32 EndIndex = (Num * (ThreadIndex + 1)) / Workers;
for (int32 Index = StartIndex; Index < EndIndex; ++Index) {
if (IsValid(Members[Index])) {
Members[Index]->AioTick();
}
}
}, false);
}
8 Replies
FreakinaBox
FreakinaBoxOPβ€’2mo ago
My Tickable Actor
C++
class ALIENIO_API UAIO_ComponentBase : public UActorComponent, public IAIO_Tickable {
GENERATED_BODY()
public:
UAIO_ComponentBase() {
// made my own that doesnt turn off when i am not close!
PrimaryComponentTick.bCanEverTick = false;
}

UFUNCTION()
virtual void AioTick() override {
//Do your thing in your own thread
}

protected:
UPROPERTY()
TObjectPtr<AAIO_SubsystemBase> Subsystem;
// use me when you need to make sure that 2 threads dont access the same thing at the same time
FCriticalSection InventoryLock;

virtual void BeginPlay() override {
Super::BeginPlay();

// register with subsystem
const auto World = GetWorld();
if (!IsValid(World)) return;
const auto SubsystemActorManager = World->GetSubsystem<USubsystemActorManager>();
if (!IsValid(SubsystemActorManager)) return;
Subsystem = SubsystemActorManager->GetSubsystemActor<AAIO_SubsystemBase>();
if (!IsValid(Subsystem)) return;
Subsystem->RegisterManager(this);
};

virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override {
Super::EndPlay(EndPlayReason);
Subsystem->UnregisterManager(this);
}
};
C++
class ALIENIO_API UAIO_ComponentBase : public UActorComponent, public IAIO_Tickable {
GENERATED_BODY()
public:
UAIO_ComponentBase() {
// made my own that doesnt turn off when i am not close!
PrimaryComponentTick.bCanEverTick = false;
}

UFUNCTION()
virtual void AioTick() override {
//Do your thing in your own thread
}

protected:
UPROPERTY()
TObjectPtr<AAIO_SubsystemBase> Subsystem;
// use me when you need to make sure that 2 threads dont access the same thing at the same time
FCriticalSection InventoryLock;

virtual void BeginPlay() override {
Super::BeginPlay();

// register with subsystem
const auto World = GetWorld();
if (!IsValid(World)) return;
const auto SubsystemActorManager = World->GetSubsystem<USubsystemActorManager>();
if (!IsValid(SubsystemActorManager)) return;
Subsystem = SubsystemActorManager->GetSubsystemActor<AAIO_SubsystemBase>();
if (!IsValid(Subsystem)) return;
Subsystem->RegisterManager(this);
};

virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override {
Super::EndPlay(EndPlayReason);
Subsystem->UnregisterManager(this);
}
};
My Tickable Interface
C++
UINTERFACE(MinimalAPI)
class UAIO_Tickable : public UInterface {
GENERATED_BODY()
};

class ALIENIO_API IAIO_Tickable {
GENERATED_BODY()

public:
virtual void AioTick(float DeltaTime) = 0;
};
C++
UINTERFACE(MinimalAPI)
class UAIO_Tickable : public UInterface {
GENERATED_BODY()
};

class ALIENIO_API IAIO_Tickable {
GENERATED_BODY()

public:
virtual void AioTick(float DeltaTime) = 0;
};
Kyrium
Kyriumβ€’2mo ago
You can significant improve the performance if you tick a lot of actors in this way if you group them and limit the threats otherwise it overflows the queue. thats some code from RP solar panel subsystem . it also comes closer to the FactoryTick
void ARPSolarSubsystem::UpdateSolarPanelsRotation(const float dt)
{
FCriticalSection Mutex;
if(bSolarPanelsAreDirty)
{
mSolarPanelGroups.Empty();
mSolarPanelGroups.SetNum( 100 );

if(mSolarPanels.Num() <= 0)
{
return;
}

const int32 NumPerGroup = FMath::Max( FMath::DivideAndRoundUp( mSolarPanels.Num(), 8 ), 1 );
ParallelFor( 8, [&, NumPerGroup](const int32 Index)
{
TArray<TArray<ARPSolarPanel*>> Grouping;
Grouping.SetNum( 100 );
for(int32 Member = Index * NumPerGroup; Member < FMath::Min( (Index + 1) * NumPerGroup, mSolarPanels.Num() ); Member++)
{
const int32 GroupIndex = fmod( Index, 100 );
Grouping[GroupIndex].Add( mSolarPanels[Member] );
}

for(int i = 0; i < Grouping.Num(); ++i)
{
if(Grouping[i].Num() <= 0)
{
continue;
}

Mutex.Lock();
mSolarPanelGroups[i].Append( Grouping[i] );
Mutex.Unlock();
}
} );
}

for(int32 I = 0; I < mSolarPanelGroups.Num(); ++I)
{
bool Dirty = bSolarPanelsAreDirty;
if(mSolarPanelGroups[I].Num() == 0)
{
continue;
}

const int32 NumPerGroup = FMath::Max( FMath::DivideAndRoundUp( mSolarPanelGroups[I].Num(), 8 ), 1 );
ParallelFor( 8, [&, I, BuildableSubsystem, Allowed, Dirty](const int32 Index)
{
for(int32 Member = Index * NumPerGroup; Member < FMath::Min( (Index + 1) * NumPerGroup, mSolarPanelGroups[I].Num() ); Member++)
{
mSolarPanelGroups[I][Member].CustomTick(dt);
}
} );
}

bSolarPanelsAreDirty = false;
}

void ARPSolarSubsystem::OnSolarPanelBuild(ARPSolarPanel* SolarPanel)
{
if(ensureAlways( SolarPanel ))
{
if(mSolarPanels.Add( SolarPanel ) != INDEX_NONE)
{
bSolarPanelsAreDirty = true;
}
}
}
void ARPSolarSubsystem::UpdateSolarPanelsRotation(const float dt)
{
FCriticalSection Mutex;
if(bSolarPanelsAreDirty)
{
mSolarPanelGroups.Empty();
mSolarPanelGroups.SetNum( 100 );

if(mSolarPanels.Num() <= 0)
{
return;
}

const int32 NumPerGroup = FMath::Max( FMath::DivideAndRoundUp( mSolarPanels.Num(), 8 ), 1 );
ParallelFor( 8, [&, NumPerGroup](const int32 Index)
{
TArray<TArray<ARPSolarPanel*>> Grouping;
Grouping.SetNum( 100 );
for(int32 Member = Index * NumPerGroup; Member < FMath::Min( (Index + 1) * NumPerGroup, mSolarPanels.Num() ); Member++)
{
const int32 GroupIndex = fmod( Index, 100 );
Grouping[GroupIndex].Add( mSolarPanels[Member] );
}

for(int i = 0; i < Grouping.Num(); ++i)
{
if(Grouping[i].Num() <= 0)
{
continue;
}

Mutex.Lock();
mSolarPanelGroups[i].Append( Grouping[i] );
Mutex.Unlock();
}
} );
}

for(int32 I = 0; I < mSolarPanelGroups.Num(); ++I)
{
bool Dirty = bSolarPanelsAreDirty;
if(mSolarPanelGroups[I].Num() == 0)
{
continue;
}

const int32 NumPerGroup = FMath::Max( FMath::DivideAndRoundUp( mSolarPanelGroups[I].Num(), 8 ), 1 );
ParallelFor( 8, [&, I, BuildableSubsystem, Allowed, Dirty](const int32 Index)
{
for(int32 Member = Index * NumPerGroup; Member < FMath::Min( (Index + 1) * NumPerGroup, mSolarPanelGroups[I].Num() ); Member++)
{
mSolarPanelGroups[I][Member].CustomTick(dt);
}
} );
}

bSolarPanelsAreDirty = false;
}

void ARPSolarSubsystem::OnSolarPanelBuild(ARPSolarPanel* SolarPanel)
{
if(ensureAlways( SolarPanel ))
{
if(mSolarPanels.Add( SolarPanel ) != INDEX_NONE)
{
bSolarPanelsAreDirty = true;
}
}
}
FreakinaBox
FreakinaBoxOPβ€’2mo ago
Ok, So since there is a max queue size we split our for loop into 8 groups and each group runs in a separate thread. makes sense. is it worth detecting how many cores the pc has and base our thread count on that?
Rex
Rexβ€’2mo ago
I would at least have the decency of making it a named constant instead of a magic number :wonke: But I don't think detecting core/thread count would do much since you'd have to account for the CPU being used for something else This looks similar to what Digital Storage used to do for network ticks. I got rid of the 2D array to be able to use UPROPERTY and I got a substantial speedup (as I no longer had to rebuild the array every time)
Kyrium
Kyriumβ€’2mo ago
Yeah both is the same thing πŸ˜„ In RP I write that thing idk 2/3 years ago? πŸ˜‚ in DS it was Basically a copy of this. Of course is not perfect but it’s run πŸ˜„ also don’t like some parts of this code ^^ since it’s not really clean πŸ˜„
Rex
Rexβ€’2mo ago
Makes sense, just letting you know I refactored the copy in DS a while ago, so RP could benefit from it too
Kyrium
Kyriumβ€’2mo ago
Never change a running system πŸ˜‚ but yeah maybe if we need to touch that part we can also refactor this πŸ˜„
FreakinaBox
FreakinaBoxOPβ€’2mo ago
updated with my final code, I do simpler math and accept that there will be a few nullPtrs that need skipped
Want results from more Discord servers?
Add your server