Level Script not executing on tick in UE 5.6

Hello all,

I’ve come across some unexpected behaviour after upgrading my project from 5.5 to 5.6. I have a level blueprint that inherits from a custom ALevelScriptActor class. In the custom class, I have overriden the tick event, and most of the main level logic happens here. In the level blueprint itself, the tick event is empty.

In 5.5, this all worked as intended, and the logic was executed on tick. In 5.6, the logic is not executed (execution never enters the tick method of the custom ALevelScriptActor class). However, I have found that by adding anything at all to the tick event in the blueprint, the parent tick is then called and works as it did in 5.5.

I’ve checked that all tick settings are the same across both versions. Does anyone have any info about whether this is an intended change for 5.6 (I could see it being an optimisation to not tick if no logic is set to execute in the tick event) or whether it’s a bug?

Thanks.

it’s probably a side effect of this change?
" Empty Ticks no longer incur overhead in Unreal Engine 5.6 onwards."

Thanks for the reply. I am sure you’re correct on this.

While on the whole this looks like a useful optimisation, it feels like a slight oversight that a BP inherited from a class which itself contains a populated tick method won’t tick without a graph in the BP tick event.

Alot of my workflow relies on this methodoly, whereby the code executing on tick is written in the cpp class for efficiency, and the extended BP parented from this class is used to more easily populate references.

Perhaps there is a setting to prevent this from being the case?

For those running into the same issue, ensuring you have bCanEverTick = true in the parent’s constructor will prevent the BP from ignoring the empty tick function. You can see where this check occurs in the below code found in FKismetCompilerContext::SetCanEverTick()

KismetCompiler.cpp

const bool bOldFlag = TickFunction->bCanEverTick;
	// RESET FLAG 
	TickFunction->bCanEverTick = ParentTickFunction->bCanEverTick;
	
	// RECEIVE TICK
	if (!TickFunction->bCanEverTick)
	{
		// Make sure that both AActor and UActorComponent have the same name for their tick method
		static FName ReceiveTickName(GET_FUNCTION_NAME_CHECKED(AActor, ReceiveTick));
		static FName ComponentReceiveTickName(GET_FUNCTION_NAME_CHECKED(UActorComponent, ReceiveTick));

		if (const UFunction* ReceiveTickEvent = FKismetCompilerUtilities::FindOverriddenImplementableEvent(ReceiveTickName, NewClass))
		{
			// We have a tick node, but are we allowed to?

			const UEngine* EngineSettings = GetDefault<UEngine>();
			const bool bAllowTickingByDefault = EngineSettings->bCanBlueprintsTickByDefault;

			const UClass* FirstNativeClass = FBlueprintEditorUtils::FindFirstNativeClass(NewClass);
			const bool bHasCanTickMetadata = (FirstNativeClass != nullptr) && FirstNativeClass->HasMetaData(FBlueprintMetadata::MD_ChildCanTick);
			const bool bHasCannotTickMetadata = (FirstNativeClass != nullptr) && FirstNativeClass->HasMetaData(FBlueprintMetadata::MD_ChildCannotTick);
			const bool bHasUniversalParent = (FirstNativeClass != nullptr) && ((AActor::StaticClass() == FirstNativeClass) || (UActorComponent::StaticClass() == FirstNativeClass));

			if (bHasCanTickMetadata && bHasCannotTickMetadata)
			{
				// User error: The C++ class has conflicting metadata
				const FString ConlictingMetadataWarning = FText::Format(
					LOCTEXT("HasBothCanAndCannotMetadataFmt", "Native class %s has both '{0}' and '{1}' metadata specified, they are mutually exclusive and '{1}' will win."),
					FText::FromString(FirstNativeClass->GetPathName()),
					FText::FromName(FBlueprintMetadata::MD_ChildCanTick),
					FText::FromName(FBlueprintMetadata::MD_ChildCannotTick)
				).ToString();
				MessageLog.Warning(*ConlictingMetadataWarning);
			}

			// If the tick node is disconnected we can avoid ticking needlessly
			bool bIsEmptyBlueprintTick = false;
			for (const FKismetFunctionContext& FunctionContext : FunctionList)
			{
				if (FunctionContext.Function->GetName() == ReceiveTickName)
				{
					if (FunctionContext.SourceEventFromStubGraph)
					{
						UEdGraphPin* ExecThenPin = FunctionContext.SourceEventFromStubGraph->FindPin(UEdGraphSchema_K2::PN_Then);
						bIsEmptyBlueprintTick = ExecThenPin && ExecThenPin->LinkedTo.IsEmpty();
					}
					break;
				}
			}

			if (bHasCannotTickMetadata)
			{
				// This could only happen if someone adds bad metadata to AActor or UActorComponent directly
				check(!bHasUniversalParent);

				// Parent class has forbidden us to tick
				const FString NativeClassSaidNo = FText::Format(
					LOCTEXT("NativeClassProhibitsTickingFmt", "@@ is not allowed as the C++ parent class {0} has disallowed Blueprint subclasses from ticking.  Please consider using a Timer instead of Tick."),
					FText::FromString(FirstNativeClass->GetPathName())
				).ToString();
				MessageLog.Warning(*NativeClassSaidNo, FindLocalEntryPoint(ReceiveTickEvent));
			}
			else if (!bIsEmptyBlueprintTick)
			{
				if (bAllowTickingByDefault || bHasUniversalParent || bHasCanTickMetadata)
				{
					// We're allowed to tick for one reason or another
					TickFunction->bCanEverTick = true;
				}
				else
				{
					// Nothing allowing us to tick
					const FString ReceiveTickEventWarning = FText::Format(
						LOCTEXT("ReceiveTick_CanNeverTickFmt", "@@ is not allowed for Blueprints based on the C++ parent class {0}, so it will never Tick!"),
						FText::FromString(FirstNativeClass ? *FirstNativeClass->GetPathName() : TEXT("<null>"))
					).ToString();
					MessageLog.Warning(*ReceiveTickEventWarning, FindLocalEntryPoint(ReceiveTickEvent));

					const FString ReceiveTickEventRemedies = FText::Format(
						LOCTEXT("ReceiveTick_CanNeverTickRemediesFmt", "You can solve this in several ways:\n  1) Consider using a Timer instead of Tick.\n  2) Add meta=({0}) to the parent C++ class\n  3) Reparent the Blueprint to AActor or UActorComponent, which can always tick."),
						FText::FromName(FBlueprintMetadata::MD_ChildCanTick)
					).ToString();
					MessageLog.Warning(*ReceiveTickEventRemedies);
				}
			}
		}
	}

	if (TickFunction->bCanEverTick != bOldFlag)
	{
		const FCoreTexts& CoreTexts = FCoreTexts::Get();

		UE_LOG(LogK2Compiler, Verbose, TEXT("Overridden flag for class '%s': CanEverTick %s "), *NewClass->GetName(),
			TickFunction->bCanEverTick ? *(CoreTexts.True.ToString()) : *(CoreTexts.False.ToString()) );
	}
}