Setting a timer inside a UObject ?

Hey everyone,

I have a hard time trying to get a timer to work in a UObject derived class.

The code is the following:


void UYagFileServer::Start()
{
	FTimerHandle UniqueHandle;
	FTimerDelegate ListenerDelegate = FTimerDelegate::CreateUObject(this, &UYagFileServer::TCPConnectionListener);
	FileServerWorld->GetTimerManager().SetTimer(UniqueHandle, ListenerDelegate, 1.3f, true);

	if (FileServerWorld->GetTimerManager().IsTimerActive(UniqueHandle)) GEngine->AddOnScreenDebugMessage(-1, 30.f, FColor::Red, FString::Printf(TEXT("UniqueHandle active")));
}

void UYagFileServer::TCPConnectionListener()
{
	GEngine->AddOnScreenDebugMessage(-1, 30.f, FColor::Red, FString::Printf(TEXT("TCPConnectionListener")));
}

FileServerWorld is a (UWorld *) variable filled with GetWorld() by the playercontroller owning the object before calling the Start function. I have checked that it is not NULL.

Apart from the use of this variable, the same code works well when used inside a playercontroller.

But from within the UObject, the “UniqueHandle active” message is displayed, but not the one in the delegate. So the timer is active but the delegate is not called.

The code doesn’t look too fancy, any idea of what i am missing ?

Thanks

Cedric

HI,

try setting different ID for message in TCPConnectionListener. It should override previous message, but anyway, it’s probably this, since your timer is active.

Hi BiggestSmile,

Do you mean something like that ?


GEngine->AddOnScreenDebugMessage(10, 30.f, FColor::Red, FString::Printf(TEXT("TCPConnectionListener")));

This didn’t show either.

Yeah, i meant that.

In this case, try declaring UniqueHandle as class member, it’s getting destroyed after execution leaves UYagFileServer::Start() function and timer probably gets deactivated, didn’t notice this at the first look, sorry.

Hey,

Very good idea but same result unfortunately.
Just in case, here is how the FileServer object is created (ThisPC = GetFirstPlayerController() cast to my personnal PC class):


ThisPC->YagFileServer = NewObject<UYagFileServer>();
ThisPC->YagFileServer->FileServerPC = ThisPC;
ThisPC->YagFileServer->FileServerWorld = ThisPC->GetWorld();
ThisPC->YagFileServer->Start();

I am not too familiar with non-AActor management, so maybe my mistake lies somewhere in those lines (especially the first one which i am not sure about, it seems to work, but is it the right way to instanciate a UObject…) ?

Cheers

Hm, pretty strange.

Yep, it’s the right way to create UObject. I see you don’t pass outer to NewObject call, probably your YagFileServer object is being garbage collected immediately, try creating it like NewObject<UYagFileServer>(this); that should make this object referenced by the actor/object you create it within and prevent it from garbage collection at this time.

Not quite. Outer relationships do not prevent garbage collection as it is a child -> parent relationship, with the parent holding no reference to its inner objects. Also, the Outer pointer is not managed and won’t factor in reference counting either, so the child -> parent relationship won’t prevent its parent from being GC’d either.

Also, garbage collection is not instant in the “immediately” sense, i.e.: it cannot happen in the space of a single code block. It is run periodically on a new tick, so the very earliest it could happen is in the tick following the timer registration. It’s possible that it ends up being run in the 1.3 seconds before the timer expires. But I’m willing to venture that in this line:


ThisPC->YagFileServer = NewObject<UYagFileServer>();

The YagFileServer variable on the controller is managed and therefore prevents GC. (If not, that’s a very likely culprit for this issue!)

You are likely correct in that the outer should be specified. NewObject defaults to constructing an object in the transient package, which does not have an outer chain leading to the world. Instead, the world is manually passed to the UYagFileServer here, which should work, but it’s generally best practice to properly maintain the Outer chain. Especially since in this case, the Outer for YagFileServer appears quite literally to be the player controller that constructs it.

Hi,

I did try to specify the outer, but still no luck.

I also tried to lower the timer period under the tick period (ie replaced 1.3f with 0.001f), but still nothing on screen.

Reading your discussion about chaining made me think i could create the FileServer from the wrong class (i must confess the NewObject function was called from a slate widget, and slate widget are quite mysterious beings for my poor newbie self), so i moved the whole stuff in the player controller. Still no luck.

Interestingly enough i tried to display the remaining time in the PC’s tick function, and i display nothing, which seems to indicate (see the condition in the following code) that the YagServerFile object is not valid somehow.

Here is the current code (still not working, the only message i get on screen is “UniqueHandle ACTIVE”):


Slate Widget.cpp:

FReply SYagOptionsWidget::CreateServer()
{
	// get PC
	AyagPlayerController* ThisPC = Cast<AyagPlayerController>(YagHUD->GetWorld()->GetFirstPlayerController());
	if (!ThisPC) return FReply::Handled();

	ThisPC->ConsoleCommand("open WelcomeMap?listen");

	ThisPC->CreateAndStartFileServer();

	return FReply::Handled();
}

-------------------------------------------------------------------------------------------------------
PlayerController.cpp:

void AyagPlayerController::CreateAndStartFileServer()
{
	if (Role < ROLE_Authority) return;
	YagFileServer = NewObject<UYagFileServer>(this);
	YagFileServer->FileServerPC = this;
	YagFileServer->FileServerWorld = GetWorld();
	YagFileServer->Start();
}

void AyagPlayerController::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	if (GEngine)
	{
		// this condition is never satisfied, even without the IsTimerActive test.
		if (YagFileServer->IsValidLowLevel() && Role == ROLE_Authority )       //&& GetWorld()->GetTimerManager().IsTimerActive(YagFileServer->UniqueHandle))
		{
			float RemainingTime = GetWorld()->GetTimerManager().GetTimerRemaining(YagFileServer->UniqueHandle);
			GEngine->AddOnScreenDebugMessage(-1, .1f, FColor::Red, TEXT("remaining time: " + FString::SanitizeFloat(RemainingTime)));
		}
	   }
}

------------------------------------------------------------------------------------------------------
YagServer.cpp:

void UYagFileServer::Start()
{
	// ListenerDelegate and UniqueHandle are now class variables
	ListenerDelegate = FTimerDelegate::CreateUObject(this, &UYagFileServer::TCPConnectionListener);
	FileServerWorld->GetTimerManager().SetTimer(UniqueHandle, ListenerDelegate, 1.f, true);

	if (FileServerWorld->GetTimerManager().IsTimerActive(UniqueHandle)) GEngine->AddOnScreenDebugMessage(-1, 30.f, FColor::Red, FString::Printf(TEXT("UniqueHandle ACTIVE")));
}

void UYagFileServer::TCPConnectionListener()
{
	GEngine->AddOnScreenDebugMessage(10, 30.f, FColor::Red, FString::Printf(TEXT("TCPConnectionListener")));
}

Cedric

Use the debugger to inspect YagFileServer within your controller tick, where you say the condition isn’t satisfied. (i.e.: verify why it is seemingly not valid).

Also, I know Rama’s tutorials proclaim the importance of calling IsValidLowLevel all over the place, but I’ve not found this to be the case at all. If anything, it hides issues that might happen due to stale or invalid objects.

Thanks for the debug suggestion , i still don’t have those reflexes.

I couldn’t debug the tick function (it’s ticking, i can’t start the server) but i did put the condition somewhere else and got the result:

I am not sure how to read all the details but a line of zeros followed by NULL seems pretty clear :-))

What surprises me is that i still have the “uniqueHandle active” message on screen.

So the Start function is called, and the object is suppressed at once from memory ? Is that a welcome gift from the garbage collector ?

Cedric

Brr. Visual Studio en français. Je virerais naze. :slight_smile:

The garbage collector doesn’t have the power to zero out existing references. Most likely the YagFileServer was never created for the controller you are inspecting. Looking at the CreateServer function that calls CreateAndStartFileServer, you’re running an open command. Open and related console commands kick a full ServerTravel, which in the process also changes the controller. Most likely this is what’s happening, and you’re not looking at the same controller than you started out with.

I’m afraid I’m not familiar enough with the engine travelling functionality to be of much help beyond that point. I know there’s something called seamless travel which holds on to some state (IIRC, session, player state, not sure about controller) when performing a travel, which might help you. I remember seeing ShooterGame has a full server-client setup, including seamless travel.

If you don’t need this FileServer to persist during the travel, your solution might be as simple as creating it on initialization of your game player controller and/or game mode.

Il y a de quoi devenir fou :slight_smile:

I didn’t think about the travel thing either, you’re full of good ideas !

You’ve given me plenty to work on and to think about, so it’s my turn to scratch my head now !

and BiggestSmile, again, thank you very much for all your precious time, help and suggestions.

Of course, i will update as soon as i get something with that one.

Cheers

Cedric

All right, problem solved !

Once on the right track it was easy: as a test i commented out the open command and everything worked fine. From that point, the rest was a breeze, i just had to decorrelate the Start function from the open command.

Merci beaucoup for the solution, the explanation, and the debugging lesson ! Linking this problem to the open command was one super clever move, i wouldn’t have found that by myself.

Cheers !

Cedric